diff --git a/.ci/build-kit/scripts/create_ocpp_tests_image.sh b/.ci/build-kit/scripts/create_ocpp_tests_image.sh new file mode 100755 index 000000000..db2f25d44 --- /dev/null +++ b/.ci/build-kit/scripts/create_ocpp_tests_image.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +rsync -a "$EXT_MOUNT/source/tests" ./ +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Failed to copy tests" + exit $retVal +fi + +pip install --break-system-packages \ + "$EXT_MOUNT"/wheels/everestpy-*.whl \ + "$EXT_MOUNT"/wheels/everest_testing-*.whl \ + "$EXT_MOUNT"/wheels/iso15118-*.whl \ + pytest-html +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Failed to pip-install" + exit $retVal +fi + +pip install --break-system-packages -r tests/ocpp_tests/requirements.txt + +$(cd ./tests/ocpp_tests/test_sets/everest-aux/ && ./install_certs.sh "$EXT_MOUNT/dist" && ./install_configs.sh "$EXT_MOUNT/dist") diff --git a/.ci/e2e/scripts/run_ocpp_tests.sh b/.ci/e2e/scripts/run_ocpp_tests.sh new file mode 100755 index 000000000..5b41df0d2 --- /dev/null +++ b/.ci/e2e/scripts/run_ocpp_tests.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +cd tests + +PARALLEL_TESTS=$(nproc) + +echo "Running $PARALLEL_TESTS ocpp tests in parallel" + +pytest \ + -rA \ + -d --tx "$PARALLEL_TESTS"*popen//python=python3 \ + --max-worker-restart=0 \ + --timeout=300 \ + --junitxml="$EXT_MOUNT/ocpp-tests-result.xml" \ + --html="$EXT_MOUNT/ocpp-tests-report.html" \ + --self-contained-html \ + ocpp_tests/test_sets/ocpp16/*.py \ + ocpp_tests/test_sets/ocpp201/*.py \ + --everest-prefix "$EXT_MOUNT/dist" +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "OCPP tests failed with return code $retVal" + exit $retVal +fi diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 6a878c5f5..3a6cd3e5a 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -29,3 +29,94 @@ jobs: do_not_run_coverage_badge_creation: true run_install_wheels: true run_integration_tests: true + ocpp-tests: + name: OCPP Tests + needs: + - ci + runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} + steps: + - name: Download dist dir + uses: actions/download-artifact@v4.1.8 + with: + name: dist + - name: Extract dist.tar.gz + run: | + tar -xzf ${{ github.workspace }}/dist.tar.gz -C ${{ github.workspace }} + - name: Download wheels + # if: ${{ inputs.run_install_wheels == 'true' }} + uses: actions/download-artifact@v4.1.8 + with: + name: wheels + path: wheels + - name: Checkout repository + uses: actions/checkout@v4.2.2 + with: + path: source + - name: Setup run scripts + run: | + mkdir scripts + rsync -a source/.ci/build-kit/scripts/ scripts + - name: Docker Meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.BUILD_KIT_IMAGE_NAME }} + - name: Set output tag + id: buildkit_tag + shell: python3 {0} + run: | + import os + tags = "${{ steps.meta.outputs.tags }}".split(",") + if len(tags) == 0: + print("No tags found!❌") + exit(1) + tag = f"local/build-kit-everest-core:{tags[0]}" + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"tag={tag}\n") + print(f"Set tag={tag}") + - name: Download build-kit image + uses: actions/download-artifact@v4 + with: + name: build-kit + - name: Load build-kit image + run: | + docker load -i build-kit.tar + docker image tag ${{ steps.buildkit_tag.outputs.tag }} build-kit + - name: Create integration-image + run: | + docker run \ + --volume "${{ github.workspace }}:/ext" \ + --name integration-container \ + build-kit run-script create_ocpp_tests_image + docker commit integration-container integration-image + - name: Run OCPP tests + id: run_ocpp_tests + continue-on-error: true + run: | + docker compose \ + -f source/.ci/e2e/docker-compose.yaml \ + run \ + e2e-test-server \ + run-script run_ocpp_tests + - name: Upload result and report as artifact + continue-on-error: true + if: ${{ steps.run_ocpp_tests.outcome == 'success' || steps.run_ocpp_tests.outcome == 'failure' }} + uses: actions/upload-artifact@v4.4.3 + with: + if-no-files-found: error + name: ocpp-tests-report + path: | + ocpp-tests-result.xml + ocpp-tests-report.html + - name: Render OCPP tests result + if: ${{ steps.run_ocpp_tests.outcome == 'success' || steps.run_ocpp_tests.outcome == 'failure' }} + uses: pmeier/pytest-results-action@v0.7.1 + with: + path: ocpp-tests-result.xml + summary: True + display-options: fEX + fail-on-empty: True + title: Test results + - name: Check if OCPP tests failed + if: ${{ steps.run_ocpp_tests.outcome == 'failure' }} + run: exit 1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f4730ee9..d5c318531 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,7 +169,7 @@ add_custom_target(install_everest_testing if [ -z "${CPM_PACKAGE_everest-utils_SOURCE_DIR}" ] \; then echo "Could not determine location of everest-utils, please install everest-testing manually!" \; else echo "Found everest-utils at ${CPM_PACKAGE_everest-utils_SOURCE_DIR}" \; - ${PYTHON_EXECUTABLE} -m pip install "${CPM_PACKAGE_everest-utils_SOURCE_DIR}/everest-testing" \; + ${Python3_EXECUTABLE} -m pip install -e "${CPM_PACKAGE_everest-utils_SOURCE_DIR}/everest-testing" \; fi\; DEPENDS everestpy_pip_install_dist diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index d279bcb45..779d59300 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -23,6 +23,7 @@ active_modules: config_module: connector_id: 1 evse_id: "1" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp/everest-logs @@ -55,6 +56,7 @@ active_modules: config_module: connector_id: 2 evse_id: "2" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp diff --git a/config/config-sil-ocpp201-pnc.yaml b/config/config-sil-ocpp201-pnc.yaml index f3f8ac76a..7556d44b1 100644 --- a/config/config-sil-ocpp201-pnc.yaml +++ b/config/config-sil-ocpp201-pnc.yaml @@ -143,6 +143,9 @@ active_modules: implementation_id: external_limits - module_id: evse_manager_2_ocpp_sink implementation_id: external_limits + reservation: + - module_id: auth + implementation_id: reservation evse_security: module: EvseSecurity config_module: diff --git a/config/config-sil-ocpp201.yaml b/config/config-sil-ocpp201.yaml index 2ae3bcfdf..fe537cf15 100644 --- a/config/config-sil-ocpp201.yaml +++ b/config/config-sil-ocpp201.yaml @@ -21,6 +21,7 @@ active_modules: config_module: connector_id: 1 evse_id: "1" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp @@ -49,6 +50,7 @@ active_modules: config_module: connector_id: 2 evse_id: "2" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp @@ -142,6 +144,9 @@ active_modules: implementation_id: external_limits - module_id: evse_manager_2_ocpp_sink implementation_id: external_limits + reservation: + - module_id: auth + implementation_id: reservation persistent_store: module: PersistentStore evse_security: @@ -169,6 +174,9 @@ active_modules: implementation_id: evse - module_id: evse_manager_2 implementation_id: evse + kvs: + - module_id: persistent_store + implementation_id: main energy_manager: module: EnergyManager connections: diff --git a/dependencies.yaml b/dependencies.yaml index 4cba29e37..ddaf4156c 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -67,7 +67,7 @@ libevse-security: # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: v0.20.0 + git_tag: 9836ac4766e99a79555adb15c3001c8704f8b7a7 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBOCPP" # Josev Josev: @@ -86,7 +86,7 @@ ext-mbedtls: # everest-testing and ev-dev-tools everest-utils: git: https://github.com/EVerest/everest-utils.git - git_tag: v0.4.2 + git_tag: v0.4.3 # unit testing gtest: diff --git a/interfaces/reservation.yaml b/interfaces/reservation.yaml index 0cba39f43..615f00a06 100644 --- a/interfaces/reservation.yaml +++ b/interfaces/reservation.yaml @@ -1,21 +1,15 @@ description: Interface for reservations cmds: reserve_now: - description: Reserves this evse. + description: Reserves an evse. arguments: - connector_id: - description: >- - The id of the connector to be reserved. A value of 0 means that - the reservation is not for a specific connector - type: integer - reservation: - description: The information about the Reservation to be placed + request: type: object $ref: /reservation#/Reservation + description: Requests to make a reservation result: description: >- - Returns Accepted if reservation was succesfull or specifies error - code. + Returns Accepted if reservation was succesful or specifies error code. type: string $ref: /reservation#/ReservationResult cancel_reservation: @@ -29,4 +23,24 @@ cmds: Returns true if reservation was cancelled. Returns false if there was no reservation to cancel. type: boolean -vars: {} + exists_reservation: + description: >- + Checks if there is a reservation made for the given connector and token. Will also return true if there + is a reservation with this token for evse id 0. + arguments: + request: + type: object + $ref: /reservation#/ReservationCheck + description: >- + The information to send for the check if there is a reservation on the given connector for the given token. + result: + description: >- + Returns an enum which indicates the reservation status of the given id / id token / group id token combination. + type: string + $ref: /reservation#/ReservationCheckStatus +vars: + reservation_update: + description: >- + Update of the reservation. + type: object + $ref: /reservation#/ReservationUpdateStatus diff --git a/lib/staging/ocpp/ocpp_conversions.cpp b/lib/staging/ocpp/ocpp_conversions.cpp index 04af75fb6..7c731171d 100644 --- a/lib/staging/ocpp/ocpp_conversions.cpp +++ b/lib/staging/ocpp/ocpp_conversions.cpp @@ -367,4 +367,22 @@ ocpp::DateTime to_ocpp_datetime_or_now(const std::string& datetime_string) { } return ocpp::DateTime(); } + +ocpp::ReservationCheckStatus +to_ocpp_reservation_check_status(const types::reservation::ReservationCheckStatus& status) { + switch (status) { + case types::reservation::ReservationCheckStatus::NotReserved: + return ocpp::ReservationCheckStatus::NotReserved; + case types::reservation::ReservationCheckStatus::ReservedForToken: + return ocpp::ReservationCheckStatus::ReservedForToken; + case types::reservation::ReservationCheckStatus::ReservedForOtherToken: + return ocpp::ReservationCheckStatus::ReservedForOtherToken; + case types::reservation::ReservationCheckStatus::ReservedForOtherTokenAndHasParentToken: + return ocpp::ReservationCheckStatus::ReservedForOtherTokenAndHasParentToken; + } + + EVLOG_warning << "Could not convert reservation check status. Returning default 'NotReserved."; + return ocpp::ReservationCheckStatus::NotReserved; +} + } // namespace ocpp_conversions diff --git a/lib/staging/ocpp/ocpp_conversions.hpp b/lib/staging/ocpp/ocpp_conversions.hpp index 59d457cb3..af488e897 100644 --- a/lib/staging/ocpp/ocpp_conversions.hpp +++ b/lib/staging/ocpp/ocpp_conversions.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -49,4 +50,6 @@ types::session_cost::SessionCost create_session_cost(const ocpp::RunningCost& ru /// current datetime ocpp::DateTime to_ocpp_datetime_or_now(const std::string& datetime_string); +ocpp::ReservationCheckStatus to_ocpp_reservation_check_status(const types::reservation::ReservationCheckStatus& status); + }; // namespace ocpp_conversions diff --git a/modules/Auth/Auth.cpp b/modules/Auth/Auth.cpp index e08072038..e0cd3cdb1 100644 --- a/modules/Auth/Auth.cpp +++ b/modules/Auth/Auth.cpp @@ -14,7 +14,8 @@ void Auth::init() { this->auth_handler = std::make_unique( string_to_selection_algorithm(this->config.selection_algorithm), this->config.connection_timeout, - this->config.prioritize_authorization_over_stopping_transaction, this->config.ignore_connector_faults); + this->config.prioritize_authorization_over_stopping_transaction, this->config.ignore_connector_faults, + this->info.id, (!this->r_kvs.empty() ? this->r_kvs.at(0).get() : nullptr)); for (const auto& token_provider : this->r_token_provider) { token_provider->subscribe_provided_token([this](ProvidedIdToken provided_token) { @@ -30,20 +31,28 @@ void Auth::ready() { int32_t evse_index = 0; for (const auto& evse_manager : this->r_evse_manager) { - int32_t connector_id = evse_manager->call_get_evse().id; - this->auth_handler->init_connector(connector_id, evse_index); + const int32_t evse_id = evse_manager->call_get_evse().id; + std::vector connectors; + for (const auto& connector : evse_manager->call_get_evse().connectors) { + connectors.push_back( + Connector(connector.id, connector.type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown))); + } + + this->auth_handler->init_evse(evse_id, evse_index, connectors); - evse_manager->subscribe_session_event([this, connector_id](SessionEvent session_event) { - this->auth_handler->handle_session_event(connector_id, session_event); + evse_manager->subscribe_session_event([this, evse_id](SessionEvent session_event) { + this->auth_handler->handle_session_event(evse_id, session_event); }); evse_manager->subscribe_error( "evse_manager/Inoperative", - [this, connector_id](const Everest::error::Error& error) { - this->auth_handler->handle_permanent_fault_raised(connector_id); + // If no connector id is given, it defaults to connector id 1. + [this, evse_id](const Everest::error::Error& error) { + this->auth_handler->handle_permanent_fault_raised(evse_id, 1); }, - [this, connector_id](const Everest::error::Error& error) { - this->auth_handler->handle_permanent_fault_cleared(connector_id); + // If no connector id is given, it defaults to connector id 1. + [this, evse_id](const Everest::error::Error& error) { + this->auth_handler->handle_permanent_fault_cleared(evse_id, 1); }); evse_index++; @@ -53,6 +62,7 @@ void Auth::ready() { [this](const ProvidedIdToken& token, TokenValidationStatus status) { this->p_main->publish_token_validation_status({token, status}); }); + this->auth_handler->register_notify_evse_callback( [this](const int evse_index, const ProvidedIdToken& provided_token, const ValidationResult& validation_result) { this->r_evse_manager.at(evse_index)->call_authorize_response(provided_token, validation_result); @@ -70,11 +80,51 @@ void Auth::ready() { [this](const int32_t evse_index, const StopTransactionRequest& request) { this->r_evse_manager.at(evse_index)->call_stop_transaction(request); }); - this->auth_handler->register_reserved_callback([this](const int32_t evse_index, const int32_t& reservation_id) { - this->r_evse_manager.at(evse_index)->call_reserve(reservation_id); - }); + this->auth_handler->register_reserved_callback( + [this](const std::optional evse_id, const int32_t& reservation_id) { + // Only call the evse manager to store the reservation if it is done for a specific evse. + if (evse_id.has_value()) { + EVLOG_info << "Call reserved callback for evse id " << evse_id.value(); + + if (!this->r_evse_manager.at(evse_id.value() - 1)->call_reserve(reservation_id)) { + EVLOG_warning << "EVSE manager does not allow placing a reservation for evse id " << evse_id.value() + << ": cancelling reservation."; + this->auth_handler->handle_cancel_reservation(reservation_id); + return false; + } + } + + ReservationUpdateStatus status; + status.reservation_id = reservation_id; + status.reservation_status = Reservation_status::Placed; + this->p_reservation->publish_reservation_update(status); + return true; + }); this->auth_handler->register_reservation_cancelled_callback( - [this](const int32_t evse_index) { this->r_evse_manager.at(evse_index)->call_cancel_reservation(); }); + [this](const std::optional evse_id, const int32_t reservation_id, const ReservationEndReason reason, + const bool send_reservation_update) { + // Only call the evse manager to cancel the reservation if it was for a specific evse + if (evse_id.has_value()) { + EVLOG_debug << "Call evse manager to cancel the reservation with evse id " << evse_id.value(); + this->r_evse_manager.at(evse_id.value() - 1)->call_cancel_reservation(); + } + + if (send_reservation_update) { + ReservationUpdateStatus status; + status.reservation_id = reservation_id; + if (reason == ReservationEndReason::Expired) { + status.reservation_status = Reservation_status::Expired; + } else if (reason == ReservationEndReason::Cancelled) { + status.reservation_status = Reservation_status::Removed; + } else { + // On reservation used: do not publish a reservation update!! + return; + } + this->p_reservation->publish_reservation_update(status); + } + }); + + this->auth_handler->initialize(); } void Auth::set_connection_timeout(int& connection_timeout) { diff --git a/modules/Auth/Auth.hpp b/modules/Auth/Auth.hpp index 4f68f9303..fca11b846 100644 --- a/modules/Auth/Auth.hpp +++ b/modules/Auth/Auth.hpp @@ -18,6 +18,7 @@ #include #include #include +#include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here @@ -45,20 +46,24 @@ class Auth : public Everest::ModuleBase { std::unique_ptr p_reservation, std::vector> r_token_provider, std::vector> r_token_validator, - std::vector> r_evse_manager, Conf& config) : + std::vector> r_evse_manager, std::vector> r_kvs, + Conf& config) : ModuleBase(info), p_main(std::move(p_main)), p_reservation(std::move(p_reservation)), r_token_provider(std::move(r_token_provider)), r_token_validator(std::move(r_token_validator)), r_evse_manager(std::move(r_evse_manager)), - config(config){}; + r_kvs(std::move(r_kvs)), + config(config) { + } const std::unique_ptr p_main; const std::unique_ptr p_reservation; const std::vector> r_token_provider; const std::vector> r_token_validator; const std::vector> r_evse_manager; + const std::vector> r_kvs; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 diff --git a/modules/Auth/BUILD.bazel b/modules/Auth/BUILD.bazel index 0dbe3fc57..fd0a1c7ad 100644 --- a/modules/Auth/BUILD.bazel +++ b/modules/Auth/BUILD.bazel @@ -11,6 +11,7 @@ cc_library( "@everest-framework//:framework", "@com_github_HowardHinnant_date//:date", "//types:types_lib", + "//interfaces:interfaces_lib", ], # See https://github.com/HowardHinnant/date/issues/324 local_defines = [ diff --git a/modules/Auth/docs/index.rst b/modules/Auth/docs/index.rst index 417f27d8f..563423e59 100644 --- a/modules/Auth/docs/index.rst +++ b/modules/Auth/docs/index.rst @@ -37,6 +37,9 @@ The following diagram shows how it integrates with other EVerest modules. .. image:: everest_integration.drawio.svg :alt: Integration +The module connections of the evse_manager requirement must be connected in the correct order in the EVerest config +file, i.e. the module representing the EVSE with evse id 1 must listed first, EVSE with evse id 2 second and so on. + Selection Algorithm =================== diff --git a/modules/Auth/include/AuthHandler.hpp b/modules/Auth/include/AuthHandler.hpp index 1c493754e..971bec488 100644 --- a/modules/Auth/include/AuthHandler.hpp +++ b/modules/Auth/include/AuthHandler.hpp @@ -48,17 +48,24 @@ class AuthHandler { public: AuthHandler(const SelectionAlgorithm& selection_algorithm, const int connection_timeout, - bool prioritize_authorization_over_stopping_transaction, bool ignore_connector_faults); + bool prioritize_authorization_over_stopping_transaction, bool ignore_connector_faults, + const std::string& id, kvsIntf* store); virtual ~AuthHandler(); /** - * @brief Initializes the connector with the given \p connector_id and the given \p evse_id . It instantiates new + * @brief Initializes the evse with the given \p connectors and the given \p evse_id . It instantiates new * connector objects and fills data sturctures of the class. * - * @param connector_id + * @param evse_id * @param evse_index + * @param connectors The connectors. */ - void init_connector(const int connector_id, const int evse_index); + void init_evse(const int evse_id, const int evse_index, const std::vector& connectors); + + /** + * @brief Call when everything is initialized. This will call 'init' of the reservation handler. + */ + void initialize(); /** * @brief Handler for a new incoming \p provided_token @@ -70,50 +77,67 @@ class AuthHandler { /** * @brief Handler for new incoming \p reservation for the given \p connector . Places the reservation if possible. * - * @param connector_id * @param reservation * @return types::reservation::ReservationResult */ - types::reservation::ReservationResult handle_reservation(int connector_id, const Reservation& reservation); + types::reservation::ReservationResult handle_reservation(const Reservation& reservation); /** * @brief Handler for incoming cancel reservation request for the given \p reservation_id . * * @param reservation_id - * @return int Returns -1 if the reservation could not been cancelled, else the id of the connector. + * @return return value first returns false if the reservation could not been cancelled. Return value second is the + * evse id or nullopt if the reservation was a 'global' reservation without evse id. + */ + std::pair> handle_cancel_reservation(const int32_t reservation_id); + + /** + * @brief Callback to check if there is a reservation for the given token (on the given evse id). + * @param id_token The token to check. + * @param evse_id The evse to check the reservation for. + * @param group_id_token The group id token to check. + * @return The reservation check status */ - int handle_cancel_reservation(int reservation_id); + ReservationCheckStatus handle_reservation_exists(std::string& id_token, const std::optional& evse_id, + std::optional& group_id_token); /** * @brief Callback to signal EvseManager that the given \p connector_id has been reserved with the given \p * reservation_id . * - * @param connector_id + * @param evse_id * @param reservation_id + * + * @return true of EvseManager accepted the reservation. */ - void call_reserved(const int& connector_id, const int reservation_id); + bool call_reserved(const int reservation_id, const std::optional& evse_id); /** - * @brief Callback to signal EvseManager that the reservation for the given \p connector_id has been cancelled. + * @brief Callback to signal EvseManager that the reservation for the given \p evse_id has been cancelled. * - * @param connector_id + * @param reservation_id The id of the cancelled reservation. + * @param reason The reason the reservation was cancelled. + * @param evse_id Evse id if reservation was for a specific evse. + * @param send_reservation_update True to send a reservation update. This should not be sent if OCPP cancels + * the reservation. */ - void call_reservation_cancelled(const int& connector_id); + void call_reservation_cancelled(const int32_t reservation_id, const ReservationEndReason reason, + const std::optional& evse_id, const bool send_reservation_update); /** * @brief Handler for the given \p events at the given \p connector . Submits events to the state machine of the * handler. * - * @param connector_id + * @param evse_id * @param events */ - void handle_session_event(const int connector_id, const SessionEvent& events); + void handle_session_event(const int evse_id, const SessionEvent& events); /** * @brief Handler for permanent faults from evsemanager that prevents charging */ - void handle_permanent_fault_cleared(const int connector_id); - void handle_permanent_fault_raised(const int connector_id); + void handle_permanent_fault_cleared(const int evse_id, const int32_t connector_id); + void handle_permanent_fault_raised(const int evse_id, const int32_t connector_id); /** * @brief Set the connection timeout of the handler. @@ -172,15 +196,17 @@ class AuthHandler { * * @param callback */ - void - register_reserved_callback(const std::function& callback); + void register_reserved_callback( + const std::function& evse_id, const int& reservation_id)>& callback); /** * @brief Registers the given \p callback to signal a reservation has been cancelled to the EvseManager. * * @param callback */ - void register_reservation_cancelled_callback(const std::function& callback); + void register_reservation_cancelled_callback( + const std::function& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)>& callback); /** * @brief Registers the given \p callback to publish the intermediate token validation status. @@ -198,7 +224,7 @@ class AuthHandler { bool ignore_faults; ReservationHandler reservation_handler; - std::map> connectors; + std::map> evses; std::mutex timer_mutex; std::list plug_in_queue; @@ -207,6 +233,7 @@ class AuthHandler { std::set tokens_in_process; std::mutex token_in_process_mutex; std::condition_variable cv; + std::recursive_mutex evse_mutex; // callbacks std::function(const ProvidedIdToken& provided_token)> validate_token_callback; std::function stop_transaction_callback; std::function reservation_update_callback; - std::function reserved_callback; - std::function reservation_cancelled_callback; + std::function& evse_index, const int& reservation_id)> reserved_callback; + std::function& evse_index, const int32_t reservation_id, + const types::reservation::ReservationEndReason reason, const bool send_reservation_update)> + reservation_cancelled_callback; std::function publish_token_validation_status_callback; - std::vector get_referenced_connectors(const ProvidedIdToken& provided_token); - int used_for_transaction(const std::vector& connectors, const std::string& id_token); - bool is_token_already_in_process(const std::string& id_token, const std::vector& referenced_connectors); - bool any_connector_available(const std::vector& connectors); - bool any_parent_id_present(const std::vector connector_ids); + std::vector get_referenced_evses(const ProvidedIdToken& provided_token); + int used_for_transaction(const std::vector& evse_ids, const std::string& id_token); + bool is_token_already_in_process(const std::string& id_token, const std::vector& referenced_evses); + bool any_evse_available(const std::vector& evse_ids); + bool any_parent_id_present(const std::vector& evse_ids); bool equals_master_pass_group_id(const std::optional parent_id_token); TokenHandlingResult handle_token(const ProvidedIdToken& provided_token); /** - * @brief Method selects a connector based on the configured selection algorithm. It might block until an event - * occurs that can be used to determine a connector. + * @brief Method selects an evse based on the configured selection algorithm. It might block until an event + * occurs that can be used to determine an evse. * - * @param connectors + * @param selected_evses * @return int */ - int select_connector(const std::vector& connectors); + int select_evse(const std::vector& selected_evses); - void lock_plug_in_mutex(const std::vector& connectors); - void unlock_plug_in_mutex(const std::vector& connectors); - int get_latest_plugin(const std::vector& connectors); - void notify_evse(int connector_id, const ProvidedIdToken& provided_token, - const ValidationResult& validation_result); + void lock_plug_in_mutex(const std::vector& evse_ids); + void unlock_plug_in_mutex(const std::vector& evse_ids); + int get_latest_plugin(const std::vector& evse_ids); + void notify_evse(int evse_id, const ProvidedIdToken& provided_token, const ValidationResult& validation_result); Identifier get_identifier(const ValidationResult& validation_result, const std::string& id_token, const AuthorizationType& type); + void submit_event_for_connector(const int32_t evse_id, const int32_t connector_id, + const ConnectorEvent connector_event); }; } // namespace module diff --git a/modules/Auth/include/Connector.hpp b/modules/Auth/include/Connector.hpp index 56cd47b06..2ec2a2a0d 100644 --- a/modules/Auth/include/Connector.hpp +++ b/modules/Auth/include/Connector.hpp @@ -12,6 +12,7 @@ #include #include +#include namespace module { @@ -25,23 +26,16 @@ struct Identifier { }; struct Connector { - explicit Connector(int id) : - id(id), - transaction_active(false), - state_machine(ConnectorState::AVAILABLE), - is_reservable(true), - reserved(false){}; + explicit Connector( + int id, const types::evse_manager::ConnectorTypeEnum type = types::evse_manager::ConnectorTypeEnum::Unknown) : + id(id), transaction_active(false), state_machine(ConnectorState::AVAILABLE), type(type) { + } int id; bool transaction_active; ConnectorStateMachine state_machine; - - // identifier is set when transaction is running and none if not - std::optional identifier = std::nullopt; - - bool is_reservable; - bool reserved; + types::evse_manager::ConnectorTypeEnum type; /** * @brief Submits the given \p event to the state machine @@ -56,20 +50,39 @@ struct Connector { * @return true * @return false */ - bool is_unavailable(); + bool is_unavailable() const; ConnectorState get_state() const; }; -struct ConnectorContext { +struct EVSEContext { + + EVSEContext( + int evse_id, int evse_index, int connector_id, + const types::evse_manager::ConnectorTypeEnum connector_type = types::evse_manager::ConnectorTypeEnum::Unknown) : + evse_id(evse_id), evse_index(evse_index), transaction_active(false), plugged_in(false) { + Connector c(connector_id, connector_type); + connectors.push_back(c); + } - ConnectorContext(int connector_id, int evse_index) : evse_index(evse_index), connector(connector_id){}; + EVSEContext(int evse_id, int evse_index, const std::vector& connectors) : + evse_id(evse_id), evse_index(evse_index), transaction_active(false), connectors(connectors), plugged_in(false) { + } - int evse_index; - Connector connector; + int32_t evse_id; + int32_t evse_index; + bool transaction_active; + + // identifier is set when transaction is running and none if not + std::optional identifier = std::nullopt; + std::vector connectors; Everest::SteadyTimer timeout_timer; std::mutex plug_in_mutex; std::mutex event_mutex; + bool plugged_in; + + bool is_available(); + bool is_unavailable(); }; namespace conversions { diff --git a/modules/Auth/include/ConnectorStateMachine.hpp b/modules/Auth/include/ConnectorStateMachine.hpp index 7da984e34..1302c7b61 100644 --- a/modules/Auth/include/ConnectorStateMachine.hpp +++ b/modules/Auth/include/ConnectorStateMachine.hpp @@ -18,6 +18,7 @@ enum class ConnectorEvent { SESSION_FINISHED }; +/// @warning Do not change the order of ConnectorState, or if you do it, fix the code in ReservationHandler. enum class ConnectorState { AVAILABLE, UNAVAILABLE, diff --git a/modules/Auth/include/ReservationHandler.hpp b/modules/Auth/include/ReservationHandler.hpp index 849071646..b585bde4d 100644 --- a/modules/Auth/include/ReservationHandler.hpp +++ b/modules/Auth/include/ReservationHandler.hpp @@ -1,94 +1,321 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#ifndef RESERVATION_HANDLER_HPP -#define RESERVATION_HANDLER_HPP +#pragma once +#include +#include +#include +#include #include #include #include +#include #include -#include + +class kvsIntf; namespace module { class ReservationHandler { +private: // Members + /// \brief Map of EVSE's, with EVSE id as key and the EVSE struct as value. + std::map>& evses; + /// \brief Recursive mutex for the evse map (shared with AuthHandler). + std::recursive_mutex& evse_mutex; + /// \brief Key value store id. + const std::string kvs_store_key_id; + /// \brief Key value store for storing reservations. + kvsIntf* store; + /// \brief Map of EVSE specific reservations, with EVSE id as key and the Reservation type as value. + std::map evse_reservations; + /// \brief All reservations not bound to a specific EVSE. + std::vector global_reservations; + /// \brief Reservation mutex, for all reservation bound locks. + mutable std::recursive_mutex reservation_mutex; + /// \brief Timer mutex, for all timer bound locks (for `reservation_id_to_reservation_timeout_timer_map`) + mutable std::recursive_mutex timer_mutex; + /// \brief Map with reservations and their timer. + /// + /// Every reservation has a specific end time, which is stored in this map. Key is the reservation id. When the + /// timer expires, it is removed from the map and the reservation is removed from the `evse_reservations` or + /// `global_reservations`. + std::map> reservation_id_to_reservation_timeout_timer_map; -private: - std::map reservations; + /// \brief The callback that is called when a reservation is cancelled. + std::function& evse_id, const int32_t reservation_id, + const types::reservation::ReservationEndReason reason, const bool send_reservation_update)> + reservation_cancelled_callback; - std::mutex timer_mutex; - std::mutex reservation_mutex; - std::map> connector_to_reservation_timeout_timer_map; - - std::function reservation_cancelled_callback; + /// \brief worker for the timers. + boost::shared_ptr work; + /// \brief io_service for the worker for the timers. + boost::asio::io_service io_service; + /// \brief io service thread for the timers. + std::thread io_service_thread; public: - /** - * @brief Initializes a connector with the given \p connector_id . This creates an entry in the map of timers of the - * handler. - * - * @param connector_id - */ - void init_connector(int connector_id); - - /** - * @brief Function checks if the given \p id_token or \p parent_id_token matches the reserved token of the given \p - * connector - * - * @param connector - * @param id_token - * @param parent_id_token - * @return true - * @return false - */ - bool matches_reserved_identifier(int connector, const std::string& id_token, - std::optional parent_id_token); - - /** - * @brief Functions check if reservation at the given \p connector contains a parent_id - * @param connector - * @return true if reservation for \p connector exists and reservation contains a parent_id - */ - bool has_reservation_parent_id(int connector); - - /** - * @brief Function tries to reserve the given \p connector using the given \p reservation - * - * @param connector - * @param state Current state of the connector - * @param is_reservable - * @param reservation - * @return types::reservation::ReservationResult - */ - types::reservation::ReservationResult reserve(int connector, const ConnectorState& state, bool is_reservable, - const types::reservation::Reservation& reservation); - - /** - * @brief Function tries to cancel reservation with the given \p reservation_id . - * - * @param reservation_id - * @param execute_callback if true, cancel_reservation_callback will be executed - * @return int -1 if reservation could not been cancelled, else the id of the connnector - */ - int cancel_reservation(int reservation_id, bool execute_callback); - - /** - * @brief Handler that is called when a reservation was started / used. - * - * @param connector - */ - void on_reservation_used(int connector); - - /** - * @brief Registers the given \p callback that is called when a reservation should be cancelled. - * - * @param callback - */ - void register_reservation_cancelled_callback(const std::function& callback); + /// + /// \brief Constructor. + /// + ReservationHandler(std::map>& evses, std::recursive_mutex& evse_mutex, + const std::string& id, kvsIntf* store); + + /// + /// \brief Destructor. + /// + ~ReservationHandler(); + + /// + /// \brief Load reservations from key value store. + /// + void load_reservations(); + + /// + /// \brief Try to make a reservation. + /// \param evse_id Optional, the evse id. If omitted, a 'global' reservation will be made. + /// \param reservation The reservation to make. + /// \return The result of the reservation (`Accepted` if the reservation could be made). + /// + types::reservation::ReservationResult make_reservation(const std::optional evse_id, + const types::reservation::Reservation& reservation); + + /// + /// \brief Change a specific connector state. + /// + /// This is important for the reservation handler, to know which connector is in which state, to know if a + /// reservation can be made or not. + /// + /// \param connector_state The state of the connector. + /// \param evse_id The EVSE id the connector belongs to. + /// \param connector_id The connector id. + /// + void on_connector_state_changed(const ConnectorState connector_state, const uint32_t evse_id, + const uint32_t connector_id); + + /// + /// \brief Check if charging is possible on a given EVSE. + /// + /// If there are multiple global reservations, while a charging station might look available, it is possible that + /// charging is not possible because then cars that made reservations can not charge anymore. + /// + /// Only use this function to check if a car can charge without having a reservation id. + /// + /// \param evse_id The evse on which a car wants to charge. + /// \return True if charging is possible. + /// + bool is_charging_possible(const uint32_t evse_id); + + /// + /// \brief Check is an EVSE is reserved. + /// + /// This only looks at EVSE specific reservations. + /// + /// \param evse_id The evse id to check. + /// \return True if EVSE is reserved. + /// + bool is_evse_reserved(const uint32_t evse_id); + + /// + /// \brief Cancel a reservation. + /// \param reservation_id The id of the reservation to cancel. + /// \param execute_callback True if the `reservation_cancelled_callback` must be called. + /// \param reason The cancel reason. + /// \return First: true if reservation could be cancelled. + /// Second: The evse id if the reservation to cancel was made for a specific EVSE. + /// + std::pair> cancel_reservation(const int reservation_id, const bool execute_callback, + const types::reservation::ReservationEndReason reason); + + /// + /// \brief Register reservation cancelled callback. + /// \param callback The callback that should be called when a reservation is cancelled. + /// + void register_reservation_cancelled_callback( + const std::function& evse_id, const int32_t reservation_id, + const types::reservation::ReservationEndReason reason, + const bool send_reservation_update)>& callback); + + /// + /// \brief Called when a reservation is used, will remove it from the reservation list. + /// \param reservation_id The if of the reservation that is used. + /// + /// \note This will not set the EVSE or Connector to 'available'. That must be done separately (because we don't + /// know here when the connector is not connected anymore). + /// + void on_reservation_used(const int32_t reservation_id); + + /// + /// @brief Function checks if the given \p id_token or \p parent_id_token matches the reserved token of the given \p + /// evse_id + /// + /// @param id_token Id token + /// @param evse_id Evse id + /// @param parent_id_token Parent id token + /// @return The reservation id when there is a matching identifier, otherwise std::nullopt. + /// + std::optional matches_reserved_identifier(const std::string& id_token, + const std::optional evse_id, + std::optional parent_id_token); + + /// + /// @brief Functions check if reservation at the given \p evse_id contains a parent_id + /// @param evse_id Evse id + /// @return true if reservation for \p evse_id exists and reservation contains a parent_id + /// + bool has_reservation_parent_id(const std::optional evse_id); + +private: // Functions + /// + /// \brief Check if there is a specific connector type in the vector. + /// \param evse_connectors The vector to check for the type. + /// \param connector_type The connector type to find. + /// \return True if the connector type is in the vector. + /// + bool has_evse_connector_type(const std::vector& evse_connectors, + const types::evse_manager::ConnectorTypeEnum connector_type) const; + + /// + /// \brief Check if there is at least one EVSE with the given connector type. + /// \param connector_type The connector type to check. + /// \return True if at least one EVSE has this connector type. + /// + bool does_evse_connector_type_exist(const types::evse_manager::ConnectorTypeEnum connector_type) const; + + /// + /// \brief Helper function to get a reservation result from the current EVSE state and connector state, and if + /// there is a specific reservation for this EVSE. + /// \param evse_id The evse id to get the state from. + /// \param evse_specific_reservations The evse specific reservations list to look in. + /// \return The `ReservationResult` to return for this specific EVSE. + /// + types::reservation::ReservationResult get_evse_connector_state_reservation_result( + const uint32_t evse_id, const std::map& evse_specific_reservations); + + /// + /// \brief Helper function to check if the connector of a specific EVSE is available. + /// \param evse_id The evse id the connector belongs to. + /// \param connector_type The connector type to check. + /// \return The `ReservationResult` to return for his specific connector. + /// + types::reservation::ReservationResult + get_connector_availability_reservation_result(const uint32_t evse_id, + const types::evse_manager::ConnectorTypeEnum connector_type); + + /// + /// \brief Get all possible orders of connector types given a vector of connector types. + /// + /// For the reservations, there must be checked if all combinations of arriving cars with specific connector types + /// are possible. For that, we want to have a list of all different combinations of arriving. + /// + /// So for example for connector type A, B and C, the different combinations are: + /// - A, B, C + /// - A, C, B + /// - B, A, C + /// - B, C, A + /// - C, A, B + /// - C, B, A + /// + /// And for connector types A, A and B, the combinations are: + /// - A, A, B + /// - A, B, A + /// - B, A, A + /// + /// \param connectors The connector types to get all orders from. + /// \return A vector of all orders of the connector types. + /// + std::vector> + get_all_possible_orders(const std::vector& connectors) const; + + /// + /// \brief Helper function: For a specific order of arrival of cars, check if there is still an EVSE available for + /// each car. + /// + /// This function is called recursively, until no 'virtual cars' are left. + /// + /// \param used_evse_ids The evse id's we have used in previous checks. This will be empty when the + /// function is first called and will be filled every time an evse id is + /// 'used'. + /// \param next_car_arrival_order The order in which the cars arrive. This is for example 'A, B, C' and as + /// soon as the first is handled, it is removed from the list before + /// recursively calling the function again. + /// \param evse_specific_reservations EVSE specific reservations, to see if an EVSE is already reserved. + /// \return True if this combination of car arrival orders is possible. + /// + bool can_virtual_car_arrive(const std::vector& used_evse_ids, + const std::vector& next_car_arrival_order, + const std::map& evse_specific_reservations); + + /// + /// \brief Check if it is possible to make a new reservation. + /// \param global_reservation_type If it is a global reservation: the reservation type. + /// \param reservations_no_evse The list of global reservations. + /// \param evse_specific_reservations The list of evse specific reservations. + /// \return True if a reservation is possible, otherwise false. + /// + bool is_reservation_possible(const std::optional global_reservation_type, + const std::vector& reservations_no_evse, + const std::map& evse_specific_reservations); + + /// + /// \brief If a reservation is made, add the reservation to the `reservation_id_to_reservation_timeout_timer_map`. + /// \param reservation The reservation. + /// \param evse_id The evse id. + /// + void set_reservation_timer(const types::reservation::Reservation& reservation, + const std::optional evse_id); + + /// + /// \brief Get all evses that have a specific connector type. + /// \param connector_type The connector type. + /// \return Vector with evse's. + /// + std::vector + get_all_evses_with_connector_type(const types::evse_manager::ConnectorTypeEnum connector_type) const; + + /// + /// \brief For H01.FR.11, H01.FR.12 and H01.FR.13, the correct state must be returned. + /// + /// Also see @see module::ReservationHandler::get_reservation_evse_connector_state. This is a helper function to + /// return the 'more important' state (Occupied is 'more important' than Unavailable). + /// + /// \param currrent_state The current connector state. + /// \param new_state The new state. + /// \return The connector state. + /// + ConnectorState get_new_connector_state(ConnectorState currrent_state, const ConnectorState new_state) const; + + /// + /// \brief For H01.FR.11, H01.FR.12 and H01.FR.13, the correct state must be returned: if (all) evses are Occupied + /// or reserved, occupied must be returned, if (all) evses are Faulted, faulted must be returned, if (all) + /// evses are unavailable, unavailable must be returned. This function helps returning the correct state. + /// + /// If at least one of the EVSE's is Occupied, it will return occupied, then it will look to faulted and then to + /// unavailable. So if one is occupied and one faulted, it will still return occupied. + /// + /// \param connector_type The connector type to check. + /// \return The reservation result that can be returned on the reserve now request. + /// + types::reservation::ReservationResult + get_reservation_evse_connector_state(const types::evse_manager::ConnectorTypeEnum connector_type) const; + + /// + /// \brief After a connector or evse is set to unavailable, faulted or occupied, this function can be called to + /// check the reservations and cancel reservations that are not possible now anymore. + /// + void check_reservations_and_cancel_if_not_possible(); + + /// + /// \brief Store reservations to key value store. + /// + void store_reservations(); + + /// + /// \brief Helper function to print information about reservations and evses, to find out why a reservation has + /// failed. + /// \param reservation The reservation. + /// \param evse_id The evse id. + /// + void print_reservations_debug_info(const types::reservation::Reservation& reservation, + const std::optional evse_id, const bool reservation_failed); }; } // namespace module - -#endif // RESERVATION_HANDLER_HPP diff --git a/modules/Auth/lib/AuthHandler.cpp b/modules/Auth/lib/AuthHandler.cpp index 2e2e7045f..5b3844757 100644 --- a/modules/Auth/lib/AuthHandler.cpp +++ b/modules/Auth/lib/AuthHandler.cpp @@ -2,7 +2,9 @@ // Copyright Pionix GmbH and Contributors to EVerest #include + #include +#include namespace module { @@ -38,19 +40,32 @@ std::string token_handling_result_to_string(const TokenHandlingResult& result) { } // namespace conversions AuthHandler::AuthHandler(const SelectionAlgorithm& selection_algorithm, const int connection_timeout, - bool prioritize_authorization_over_stopping_transaction, bool ignore_faults) : + bool prioritize_authorization_over_stopping_transaction, bool ignore_faults, + const std::string& id, kvsIntf* store) : selection_algorithm(selection_algorithm), connection_timeout(connection_timeout), prioritize_authorization_over_stopping_transaction(prioritize_authorization_over_stopping_transaction), - ignore_faults(ignore_faults){}; + ignore_faults(ignore_faults), + reservation_handler(evses, evse_mutex, id, store) { +} AuthHandler::~AuthHandler() { } -void AuthHandler::init_connector(const int connector_id, const int evse_index) { - std::unique_ptr ctx = std::make_unique(connector_id, evse_index); - this->connectors.emplace(connector_id, std::move(ctx)); - this->reservation_handler.init_connector(connector_id); +void AuthHandler::init_evse(const int evse_id, const int evse_index, const std::vector& connectors) { + EVLOG_debug << "Add evse with evse id " << evse_id; + + if (evse_id < 0) { + EVLOG_error << "Can not add connector to reservation handler: evse id is negative."; + return; + } + + std::unique_lock lock(evse_mutex); + this->evses[evse_id] = std::make_unique(evse_id, evse_index, connectors); +} + +void AuthHandler::initialize() { + this->reservation_handler.load_reservations(); } TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) { @@ -60,15 +75,15 @@ TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) // check if token is already currently processed EVLOG_info << "Received new token: " << provided_token; this->token_in_process_mutex.lock(); - const auto referenced_connectors = this->get_referenced_connectors(provided_token); + const auto referenced_evses = this->get_referenced_evses(provided_token); - if (!this->is_token_already_in_process(provided_token.id_token.value, referenced_connectors)) { + if (!this->is_token_already_in_process(provided_token.id_token.value, referenced_evses)) { // process token if not already in process this->tokens_in_process.insert(provided_token.id_token.value); this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::Processing); this->token_in_process_mutex.unlock(); result = this->handle_token(provided_token); - this->unlock_plug_in_mutex(referenced_connectors); + this->unlock_plug_in_mutex(referenced_evses); } else { // do nothing if token is currently processed EVLOG_info << "Received token " << provided_token.id_token.value << " repeatedly while still processing it"; @@ -104,57 +119,67 @@ TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) } TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_token) { - std::vector referenced_connectors = this->get_referenced_connectors(provided_token); + std::vector 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 connector_used_for_transaction = - this->used_for_transaction(referenced_connectors, provided_token.id_token.value); - if (connector_used_for_transaction != -1) { + 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->connectors.at(connector_used_for_transaction)->evse_index, req); + 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"; 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 connector is available anyways or the provided token does not match a present + * 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 connector is available AND no parent_id is deposited at any connector and no master pass group id is + /* 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_connector_available(referenced_connectors) and - !this->any_parent_id_present(referenced_connectors) and !this->master_pass_group_id.has_value()) { + 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 connectors are reserved and the given identifier doesnt match any reserved identifier and no parent id is + /* 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_connectors_reserved_and_tag_does_not_match = true; - for (const auto connector_id : referenced_connectors) { - const auto connector = this->connectors.at(connector_id)->connector; - if (!connector.reserved) { - all_connectors_reserved_and_tag_does_not_match = false; + 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(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; } - if (this->reservation_handler.matches_reserved_identifier(connector_id, provided_token.id_token.value, - std::nullopt)) { - all_connectors_reserved_and_tag_does_not_match = false; + + const std::optional 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(connector_id)) { - all_connectors_reserved_and_tag_does_not_match = false; + if (this->reservation_handler.has_reservation_parent_id(evse_id_u)) { + all_evses_reserved_and_tag_does_not_match = false; break; } } - if (all_connectors_reserved_and_tag_does_not_match) { + if (all_evses_reserved_and_tag_does_not_match) { return TokenHandlingResult::NO_CONNECTOR_AVAILABLE; } @@ -172,8 +197,8 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to bool attempt_stop_with_parent_id_token = false; if (this->prioritize_authorization_over_stopping_transaction) { - // check if any connector is available - if (!this->any_connector_available(referenced_connectors)) { + // 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; } @@ -189,13 +214,14 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to 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!"; - for (const auto connector_id : referenced_connectors) { - const auto connector = this->connectors.at(connector_id)->connector; - if (connector.transaction_active) { + + std::unique_lock lock(evse_mutex); + 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->connectors.at(connector_id)->evse_index, req); + this->stop_transaction_callback(this->evses.at(evse_id)->evse_index, req); } } // TOOD: Add handling in case there is a display which can be used which transaction should stop @@ -203,19 +229,17 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to return TokenHandlingResult::USED_TO_STOP_TRANSACTION; } - const auto connector_used_for_transaction = - this->used_for_transaction(referenced_connectors, validation_result.parent_id_token.value().value); - if (connector_used_for_transaction != -1) { - const auto connector = this->connectors.at(connector_used_for_transaction)->connector; - // only stop transaction if a transaction is active - if (!connector.transaction_active) { + 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) { + std::unique_lock lock(evse_mutex); + 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->connectors.at(connector_used_for_transaction)->evse_index, - req); + 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"; return TokenHandlingResult::USED_TO_STOP_TRANSACTION; } @@ -224,8 +248,8 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to } } - // check if any connector is available - if (!this->any_connector_available(referenced_connectors)) { + // check if any evse is available + if (!this->any_evse_available(referenced_evses)) { return TokenHandlingResult::NO_CONNECTOR_AVAILABLE; } @@ -234,55 +258,61 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to bool authorized = false; std::vector::size_type i = 0; // iterate over validation results - while (i < validation_results.size() && !authorized && !referenced_connectors.empty()) { + 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!"; + "to authorize this token for starting transactions!"; return TokenHandlingResult::REJECTED; } this->publish_token_validation_status_callback(provided_token, types::authorization::TokenValidationStatus::Accepted); /* although validator accepts the authorization request, the Auth module still needs to - - select the connector for the authorization request + - select the evse for the authorization request - process it against placed reservations - - compare referenced_connectors against the connectors listed in the validation_result + - compare referenced_evses against the evses listed in the validation_result */ - int connector_id = this->select_connector(referenced_connectors); // might block - EVLOG_debug << "Selected connector#" << connector_id << " for token: " << provided_token.id_token.value; - if (connector_id != -1) { // indicates timeout of connector selection + int evse_id = this->select_evse(referenced_evses); // might block + EVLOG_debug << "Selected evse#" << evse_id << " for token: " << provided_token.id_token.value; + if (evse_id != -1) { // indicates timeout of evse selection + std::optional parent_id_token; + if (validation_result.parent_id_token.has_value()) { + parent_id_token = validation_result.parent_id_token.value().value; + } + const std::optional reservation_id = this->reservation_handler.matches_reserved_identifier( + provided_token.id_token.value, static_cast(evse_id), parent_id_token); + if (validation_result.evse_ids.has_value() and - intersect(referenced_connectors, validation_result.evse_ids.value()).empty()) { - EVLOG_debug - << "Empty intersection between referenced connectors and connectors that are authorized"; + 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 (!this->connectors.at(connector_id)->connector.reserved) { - EVLOG_info << "Providing authorization to connector#" << connector_id; + } else if (reservation_id == std::nullopt && + !this->reservation_handler.is_charging_possible(static_cast(evse_id))) { + validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; + } else if (!this->reservation_handler.is_evse_reserved(static_cast(evse_id)) && + (reservation_id == std::nullopt)) { + EVLOG_info << "Providing authorization to evse#" << evse_id; authorized = true; } else { - EVLOG_debug << "Connector is reserved. Checking if token matches..."; - std::optional parent_id_token; - if (validation_result.parent_id_token.has_value()) { - parent_id_token = validation_result.parent_id_token.value().value; - } - if (this->reservation_handler.matches_reserved_identifier( - connector_id, provided_token.id_token.value, parent_id_token)) { - EVLOG_info << "Connector is reserved and token is valid for this reservation"; - this->reservation_handler.on_reservation_used(connector_id); + EVLOG_debug << "Evse is reserved. Checking if token matches..."; + + if (reservation_id.has_value()) { + EVLOG_info << "Evse is reserved and token is valid for this reservation"; + this->reservation_handler.on_reservation_used(reservation_id.value()); authorized = true; + validation_result.reservation_id = reservation_id.value(); } else { - EVLOG_info << "Connector is reserved but token is not valid for this reservation"; + EVLOG_info << "Evse is reserved but token is not valid for this reservation"; validation_result.authorization_status = AuthorizationStatus::NotAtThisTime; } } - this->notify_evse(connector_id, provided_token, validation_result); + this->notify_evse(evse_id, provided_token, validation_result); } else { - // in this case we dont need / cannot notify an evse, because no connector was selected - EVLOG_info << "Timeout while selecting connector for provided token: " << provided_token; + // in this case we dont need / cannot notify an evse, because no evse was selected + EVLOG_info << "Timeout while selecting evse for provided token: " << provided_token; return TokenHandlingResult::TIMEOUT; } } @@ -294,7 +324,7 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to 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 connectors that were referenced + // 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(), @@ -310,61 +340,63 @@ TokenHandlingResult AuthHandler::handle_token(const ProvidedIdToken& provided_to } } -std::vector AuthHandler::get_referenced_connectors(const ProvidedIdToken& provided_token) { - std::vector connectors; +std::vector AuthHandler::get_referenced_evses(const ProvidedIdToken& provided_token) { + std::vector evse_ids; + std::unique_lock lock(evse_mutex); // 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(connectors), [this](int connector_id) { - if (this->connectors.find(connector_id) != this->connectors.end()) { - return !this->connectors.at(connector_id)->connector.is_unavailable(); + 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 connector_id that does not exist"; + 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->connectors) { - if (!entry.second->connector.is_unavailable()) { - connectors.push_back(entry.first); + for (const auto& entry : this->evses) { + if (!entry.second->is_unavailable()) { + evse_ids.push_back(entry.first); } } } - return connectors; + return evse_ids; } -int AuthHandler::used_for_transaction(const std::vector& connector_ids, const std::string& token) { - for (const auto connector_id : connector_ids) { - if (this->connectors.at(connector_id)->connector.identifier.has_value()) { - const auto& identifier = this->connectors.at(connector_id)->connector.identifier.value(); +int AuthHandler::used_for_transaction(const std::vector& evse_ids, const std::string& token) { + std::unique_lock lock(evse_mutex); + 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 (identifier.id_token.value == token) { - return connector_id; + return evse_id; } // check against parent_id_token else if (identifier.parent_id_token.has_value() && identifier.parent_id_token.value().value == token) { - return connector_id; + return evse_id; } } } return -1; } -bool AuthHandler::is_token_already_in_process(const std::string& id_token, - const std::vector& referenced_connectors) { +bool AuthHandler::is_token_already_in_process(const std::string& id_token, const std::vector& referenced_evses) { // checks if the token is currently already processed by the module (because already swiped) if (this->tokens_in_process.find(id_token) != this->tokens_in_process.end()) { return true; } else { + std::unique_lock lock(evse_mutex); // check if id_token was already used to authorize evse but no transaction has been started yet - for (const auto connector_id : referenced_connectors) { - const auto connector = this->connectors.at(connector_id)->connector; - if (connector.identifier.has_value() && connector.identifier.value().id_token.value == id_token && - !connector.transaction_active) { + for (const auto evse_id : referenced_evses) { + const auto& evse = this->evses.at(evse_id); + if (evse->identifier.has_value() && evse->identifier.value().id_token.value == id_token && + !evse->transaction_active) { return true; } } @@ -372,24 +404,24 @@ bool AuthHandler::is_token_already_in_process(const std::string& id_token, return false; } -bool AuthHandler::any_connector_available(const std::vector& connector_ids) { - EVLOG_debug << "Checking availability of connectors..."; - for (const auto connector_id : connector_ids) { - const auto state = this->connectors.at(connector_id)->connector.get_state(); - if (state != ConnectorState::UNAVAILABLE && state != ConnectorState::OCCUPIED && - state != ConnectorState::FAULTED) { - EVLOG_debug << "There is at least one connector available"; +bool AuthHandler::any_evse_available(const std::vector& evse_ids) { + EVLOG_debug << "Checking availability of evses..."; + std::unique_lock lock(evse_mutex); + 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 connector is available for this id_token"; + EVLOG_debug << "No evse is available for this id_token"; return false; } -bool AuthHandler::any_parent_id_present(const std::vector connector_ids) { - for (const auto connector_id : connector_ids) { - if (this->connectors.at(connector_id)->connector.identifier.has_value() and - this->connectors.at(connector_id)->connector.identifier.value().parent_id_token.has_value()) { +bool AuthHandler::any_parent_id_present(const std::vector& evse_ids) { + std::unique_lock lock(evse_mutex); + 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; } @@ -410,65 +442,65 @@ bool AuthHandler::equals_master_pass_group_id(const std::optionalmaster_pass_group_id.value(); } -int AuthHandler::get_latest_plugin(const std::vector& connectors) { +int AuthHandler::get_latest_plugin(const std::vector& evse_ids) { std::lock_guard lk(this->plug_in_queue_mutex); - for (const auto connector : this->plug_in_queue) { - if (std::find(connectors.begin(), connectors.end(), connector) != connectors.end()) { - return connector; + 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; } -void AuthHandler::lock_plug_in_mutex(const std::vector& connectors) { - for (const auto connector_id : connectors) { - this->connectors.at(connector_id)->plug_in_mutex.lock(); +void AuthHandler::lock_plug_in_mutex(const std::vector& evse_ids) { + for (const auto evse_id : evse_ids) { + this->evses.at(evse_id)->plug_in_mutex.lock(); } } -void AuthHandler::unlock_plug_in_mutex(const std::vector& connectors) { - for (const auto connector_id : connectors) { - this->connectors.at(connector_id)->plug_in_mutex.unlock(); +void AuthHandler::unlock_plug_in_mutex(const std::vector& evse_ids) { + for (const auto evse_id : evse_ids) { + this->evses.at(evse_id)->plug_in_mutex.unlock(); } } -int AuthHandler::select_connector(const std::vector& connectors) { +int AuthHandler::select_evse(const std::vector& selected_evses) { - if (connectors.size() == 1) { - return connectors.at(0); + if (selected_evses.size() == 1) { + return selected_evses.at(0); } if (this->selection_algorithm == SelectionAlgorithm::PlugEvents) { - // locks all referenced connectors for this request. Subsequent requests referencing one or more of the locked - // connectors are blocked until handle_token returns - this->lock_plug_in_mutex(connectors); - if (this->get_latest_plugin(connectors) == -1) { - // no EV has been plugged in yet at the referenced connectors - EVLOG_debug << "No connector in authorization queue. Waiting for a plug in..."; + // locks all referenced evses for this request. Subsequent requests referencing one or more of the locked + // evses are blocked until handle_token returns + this->lock_plug_in_mutex(selected_evses); + 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..."; std::unique_lock lk(this->plug_in_mutex); - // blocks until respective plugin for connector occured or until timeout + // blocks until respective plugin for evse occured or until timeout if (!this->cv.wait_for(lk, std::chrono::seconds(this->connection_timeout), - [this, connectors] { return this->get_latest_plugin(connectors) != -1; })) { + [this, selected_evses] { return this->get_latest_plugin(selected_evses) != -1; })) { return -1; } - EVLOG_debug << "Plug in at connector occured"; + EVLOG_debug << "Plug in at evse occured"; } - return this->get_latest_plugin(connectors); + return this->get_latest_plugin(selected_evses); } else if (this->selection_algorithm == SelectionAlgorithm::FindFirst) { - EVLOG_debug - << "SelectionAlgorithm FindFirst: Selecting first available connector without an active transaction"; - this->lock_plug_in_mutex(connectors); - const auto selected_connector_id = this->get_latest_plugin(connectors); - if (selected_connector_id != -1 and !this->connectors.at(selected_connector_id)->connector.transaction_active) { - // an EV has been plugged in yet at the referenced connectors - return this->get_latest_plugin(connectors); + EVLOG_debug << "SelectionAlgorithm FindFirst: Selecting first available evse without an active transaction"; + this->lock_plug_in_mutex(selected_evses); + const auto selected_evse_id = this->get_latest_plugin(selected_evses); + std::unique_lock lock(evse_mutex); + 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 + return this->get_latest_plugin(selected_evses); } else { - // no EV has been plugged in yet at the referenced connectors; choosing the first one where no + // no EV has been plugged in yet at the referenced evses; choosing the first one where no // transaction is active - for (const auto connector_id : connectors) { - const auto connector = this->connectors.at(connector_id)->connector; - if (!connector.transaction_active) { - return connector_id; + for (const auto& evse_id : selected_evses) { + const auto& evse = this->evses.at(evse_id); + if (!evse->transaction_active) { + return evse_id; } } } @@ -479,128 +511,206 @@ int AuthHandler::select_connector(const std::vector& connectors) { } } -void AuthHandler::notify_evse(int connector_id, const ProvidedIdToken& provided_token, +void AuthHandler::notify_evse(int evse_id, const ProvidedIdToken& provided_token, const ValidationResult& validation_result) { - const auto evse_index = this->connectors.at(connector_id)->evse_index; + std::unique_lock lock(evse_mutex); + 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->connectors.at(connector_id)->connector.identifier.emplace(identifier); + this->evses.at(evse_id)->identifier.emplace(identifier); std::lock_guard timer_lk(this->timer_mutex); - this->connectors.at(connector_id)->timeout_timer.stop(); - this->connectors.at(connector_id) - ->timeout_timer.timeout( - [this, evse_index, connector_id, provided_token]() { - EVLOG_info << "Authorization timeout for evse#" << evse_index; - this->connectors.at(connector_id)->connector.identifier.reset(); - this->withdraw_authorization_callback(evse_index); - this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::TimedOut); - }, - std::chrono::seconds(this->connection_timeout)); + this->evses.at(evse_id)->timeout_timer.stop(); + this->evses.at(evse_id)->timeout_timer.timeout( + [this, evse_index, evse_id, provided_token]() { + EVLOG_debug << "Authorization timeout for evse#" << evse_index; + this->evses.at(evse_id)->identifier.reset(); + this->withdraw_authorization_callback(evse_index); + this->publish_token_validation_status_callback(provided_token, TokenValidationStatus::TimedOut); + }, + std::chrono::seconds(this->connection_timeout)); std::lock_guard plug_in_lk(this->plug_in_queue_mutex); - this->plug_in_queue.remove_if([connector_id](int value) { return value == connector_id; }); + 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(int connector_id, - const Reservation& reservation) { - return this->reservation_handler.reserve(connector_id, this->connectors.at(connector_id)->connector.get_state(), - this->connectors.at(connector_id)->connector.is_reservable, reservation); +types::reservation::ReservationResult AuthHandler::handle_reservation(const Reservation& reservation) { + std::optional evse; + if (reservation.evse_id.has_value()) { + if (reservation.evse_id.value() >= 0) { + evse = static_cast(reservation.evse_id.value()); + } + } + + return reservation_handler.make_reservation(evse, reservation); } -int AuthHandler::handle_cancel_reservation(int reservation_id) { - return this->reservation_handler.cancel_reservation(reservation_id, true); +std::pair> AuthHandler::handle_cancel_reservation(const int32_t reservation_id) { + std::pair> 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(reservation_cancelled.second.value())}; + } + return {true, std::nullopt}; + } + + return {false, std::nullopt}; +} + +ReservationCheckStatus AuthHandler::handle_reservation_exists(std::string& id_token, const std::optional& evse_id, + std::optional& group_id_token) { + // Evse id has no value. + std::optional 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; } -void AuthHandler::call_reserved(const int& connector_id, const int reservation_id) { - this->reserved_callback(this->connectors.at(connector_id)->evse_index, reservation_id); +bool AuthHandler::call_reserved(const int reservation_id, const std::optional& evse_id) { + return this->reserved_callback(evse_id, reservation_id); } -void AuthHandler::call_reservation_cancelled(const int& connector_id) { - this->reservation_cancelled_callback(this->connectors.at(connector_id)->evse_index); + +void AuthHandler::call_reservation_cancelled(const int32_t reservation_id, + const types::reservation::ReservationEndReason reason, + const std::optional& evse_id, const bool send_reservation_update) { + std::optional 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); } -void AuthHandler::handle_permanent_fault_raised(const int connector_id) { +void AuthHandler::handle_permanent_fault_raised(const int evse_id, const int32_t connector_id) { if (not ignore_faults) { - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::FAULTED); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::FAULTED); } } -void AuthHandler::handle_permanent_fault_cleared(const int connector_id) { +void AuthHandler::handle_permanent_fault_cleared(const int evse_id, const int32_t connector_id) { if (not ignore_faults) { - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::ERROR_CLEARED); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ERROR_CLEARED); } } -void AuthHandler::handle_session_event(const int connector_id, const SessionEvent& event) { +void AuthHandler::handle_session_event(const int evse_id, const SessionEvent& event) { + // 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 negative: that should not be possible."; + return; + } + + if (connector_id < 0) { + EVLOG_error << "Handle session event: connector id is negative: that should not be possible."; + return; + } + + std::unique_lock lock(evse_mutex); + if (this->evses.count(evse_id) == 0) { + EVLOG_warning << "Handle session event: no evse found with evse id " << evse_id; + return; + } std::lock_guard lk(this->timer_mutex); - this->connectors.at(connector_id)->event_mutex.lock(); + this->evses.at(evse_id)->event_mutex.lock(); const auto event_type = event.event; switch (event_type) { - case SessionEventEnum::SessionStarted: - this->connectors.at(connector_id)->connector.is_reservable = false; - { - std::lock_guard lk(this->plug_in_queue_mutex); - this->plug_in_queue.push_back(connector_id); - } + case SessionEventEnum::SessionStarted: { + std::lock_guard lk(this->plug_in_queue_mutex); + this->plug_in_queue.push_back(evse_id); this->cv.notify_one(); // only set plug in timeout when SessionStart is caused by plug in if (event.session_started.value().reason == StartSessionReason::EVConnected) { - this->connectors.at(connector_id) - ->timeout_timer.timeout( - [this, connector_id]() { - EVLOG_info << "Plug In timeout for connector#" << connector_id; - this->withdraw_authorization_callback(this->connectors.at(connector_id)->evse_index); - { - std::lock_guard lk(this->plug_in_queue_mutex); - this->plug_in_queue.remove_if([connector_id](int value) { return value == connector_id; }); - } - }, - std::chrono::seconds(this->connection_timeout)); + this->evses.at(evse_id)->plugged_in = true; + + this->evses.at(evse_id)->timeout_timer.timeout( + [this, evse_id]() { + EVLOG_info << "Plug In timeout for evse#" << evse_id; + this->withdraw_authorization_callback(this->evses.at(evse_id)->evse_index); + { + std::lock_guard lk(this->plug_in_queue_mutex); + this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; }); + } + + this->evses.at(evse_id)->plugged_in = false; + }, + 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); + this->evses.at(evse_id)->timeout_timer.stop(); break; - case SessionEventEnum::TransactionStarted: - this->connectors.at(connector_id)->connector.transaction_active = true; - this->connectors.at(connector_id)->connector.reserved = false; - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::TRANSACTION_STARTED); - this->connectors.at(connector_id)->timeout_timer.stop(); - break; + } case SessionEventEnum::TransactionFinished: - this->connectors.at(connector_id)->connector.transaction_active = false; - this->connectors.at(connector_id)->connector.identifier.reset(); + this->evses.at(evse_id)->transaction_active = false; + this->evses.at(evse_id)->identifier.reset(); break; - case SessionEventEnum::SessionFinished: - this->connectors.at(connector_id)->connector.is_reservable = true; - this->connectors.at(connector_id)->connector.identifier.reset(); - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::SESSION_FINISHED); - this->connectors.at(connector_id)->timeout_timer.stop(); + case SessionEventEnum::SessionFinished: { + this->evses.at(evse_id)->plugged_in = false; + this->evses.at(evse_id)->identifier.reset(); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::SESSION_FINISHED); + this->evses.at(evse_id)->timeout_timer.stop(); { std::lock_guard lk(this->plug_in_queue_mutex); - this->plug_in_queue.remove_if([connector_id](int value) { return value == connector_id; }); + this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; }); } break; - + } case SessionEventEnum::Disabled: - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::DISABLE); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::DISABLE); break; - case SessionEventEnum::Enabled: - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::ENABLE); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::ENABLE); break; - case SessionEventEnum::ReservationStart: - this->connectors.at(connector_id)->connector.reserved = true; break; case SessionEventEnum::ReservationEnd: - this->connectors.at(connector_id)->connector.is_reservable = true; - this->connectors.at(connector_id)->connector.reserved = false; break; /// explicitly fall through all the SessionEventEnum values we are not handling case SessionEventEnum::Authorized: @@ -632,7 +742,7 @@ void AuthHandler::handle_session_event(const int connector_id, const SessionEven case SessionEventEnum::PluginTimeout: break; } - this->connectors.at(connector_id)->event_mutex.unlock(); + this->evses.at(evse_id)->event_mutex.unlock(); } void AuthHandler::set_connection_timeout(const int connection_timeout) { @@ -670,14 +780,25 @@ void AuthHandler::register_stop_transaction_callback( } void AuthHandler::register_reserved_callback( - const std::function& callback) { + const std::function& evse_id, const int& reservation_id)>& callback) { this->reserved_callback = callback; } -void AuthHandler::register_reservation_cancelled_callback(const std::function& callback) { +void AuthHandler::register_reservation_cancelled_callback( + const std::function& 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](int connector_id) { this->call_reservation_cancelled(connector_id); }); + [this](const std::optional& 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( @@ -685,4 +806,15 @@ void AuthHandler::register_publish_token_validation_status_callback( this->publish_token_validation_status_callback = callback; } +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; + } + } +} + } // namespace module diff --git a/modules/Auth/lib/Connector.cpp b/modules/Auth/lib/Connector.cpp index 63149905d..2bceff9f0 100644 --- a/modules/Auth/lib/Connector.cpp +++ b/modules/Auth/lib/Connector.cpp @@ -13,7 +13,7 @@ ConnectorState Connector::get_state() const { return this->state_machine.get_state(); } -bool Connector::is_unavailable() { +bool Connector::is_unavailable() const { return this->get_state() == ConnectorState::UNAVAILABLE || this->get_state() == ConnectorState::UNAVAILABLE_FAULTED; } @@ -38,4 +38,39 @@ std::string connector_state_to_string(const ConnectorState& state) { } } // namespace conversions + +bool EVSEContext::is_available() { + bool occupied = false; + bool available = false; + for (const auto& connector : this->connectors) { + if (connector.get_state() == ConnectorState::OCCUPIED || + connector.get_state() == ConnectorState::FAULTED_OCCUPIED) { + occupied = true; + } + if (connector.get_state() != ConnectorState::UNAVAILABLE && connector.get_state() != ConnectorState::FAULTED) { + available = true; + } + } + + if (occupied) { + // When at least one connector is occupied, they are both not available. + return false; + } + + return available; +} + +bool EVSEContext::is_unavailable() { + for (const auto& connector : this->connectors) { + if (!connector.is_unavailable()) { + return false; + } + } + + return true; +} + +// namespace conversions + +// namespace conversions } // namespace module diff --git a/modules/Auth/lib/ReservationHandler.cpp b/modules/Auth/lib/ReservationHandler.cpp index b18e23f95..c3abc0406 100644 --- a/modules/Auth/lib/ReservationHandler.cpp +++ b/modules/Auth/lib/ReservationHandler.cpp @@ -1,110 +1,808 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - #include + +#include +#include + +#include + #include +#include #include namespace module { -void ReservationHandler::init_connector(int connector_id) { - this->connector_to_reservation_timeout_timer_map[connector_id] = std::make_unique(); +static types::reservation::ReservationResult +connector_state_to_reservation_result(const ConnectorState connector_state); + +ReservationHandler::ReservationHandler(std::map>& evses, + std::recursive_mutex& evse_mutex, const std::string& id, kvsIntf* store) : + evses(evses), evse_mutex(evse_mutex), kvs_store_key_id("reservation_" + id), store(store) { + // Create this worker thread and io service etc here for the timer. + this->work = boost::make_shared(this->io_service); + this->io_service_thread = std::thread([this]() { this->io_service.run(); }); } -bool ReservationHandler::matches_reserved_identifier(int connector, const std::string& id_token, - std::optional parent_id_token) { - std::lock_guard lk(this->reservation_mutex); - // return true if id tokens match or parent id tokens exists and match - return this->reservations[connector].id_token == id_token || - (parent_id_token && this->reservations[connector].parent_id_token && - parent_id_token.value() == this->reservations[connector].parent_id_token.value()); +ReservationHandler::~ReservationHandler() { + work->get_io_context().stop(); + io_service.stop(); + io_service_thread.join(); } -bool ReservationHandler::has_reservation_parent_id(int connector) { - std::lock_guard lk(this->reservation_mutex); - if (!this->reservations.count(connector)) { - return false; +void ReservationHandler::load_reservations() { + if (this->store == nullptr) { + EVLOG_info << "Can not load reservations because the store is a nullptr."; + return; + } + + const auto stored_reservations = store->call_load(this->kvs_store_key_id); + const Array* reservations_json = std::get_if(&stored_reservations); + if (reservations_json == nullptr) { + EVLOG_warning << "Can not load reservations: reservations is not a json array."; + return; + } + + for (const auto& reservation : *reservations_json) { + types::reservation::Reservation r; + try { + r = reservation.at("reservation"); + } catch (const json::exception& e) { + EVLOG_error << "Could not get reservation from store: " << e.what(); + continue; + } + + std::optional evse_id; + if (reservation.contains("evse_id")) { + evse_id = reservation.at("evse_id"); + } + + types::reservation::ReservationResult reservation_result = this->make_reservation(evse_id, r); + if (reservation_result != types::reservation::ReservationResult::Accepted) { + EVLOG_warning << "Load reservations: Could not make reservation with id " << r.reservation_id + << ": reservation cancelled."; + this->reservation_cancelled_callback(evse_id, r.reservation_id, + types::reservation::ReservationEndReason::Cancelled, true); + } } - return this->reservations.at(connector).parent_id_token.has_value(); } -types::reservation::ReservationResult ReservationHandler::reserve(int connector, const ConnectorState& state, - bool is_reservable, - const types::reservation::Reservation& reservation) { - std::lock_guard lk(this->reservation_mutex); - if (connector == 0) { - EVLOG_info << "Reservation for connector 0 is not supported"; +types::reservation::ReservationResult +ReservationHandler::make_reservation(const std::optional evse_id, + const types::reservation::Reservation& reservation) { + if (date::utc_clock::now() > Everest::Date::from_rfc3339(reservation.expiry_time)) { + EVLOG_info << "Rejecting reservation because expire time is in the past."; return types::reservation::ReservationResult::Rejected; } - if (state == ConnectorState::UNAVAILABLE) { - EVLOG_debug << "Rejecting reservation because connector is unavailable"; - return types::reservation::ReservationResult::Unavailable; + // If a reservation was made with an existing reservation id, the existing reservation must be replaced (H01.FR.01). + // We cancel the reservation here because of that. That also means that if the reservation can not be made, the old + // reservation is cancelled anyway. + std::pair> reservation_cancelled = this->cancel_reservation( + reservation.reservation_id, false, types::reservation::ReservationEndReason::Cancelled); + if (reservation_cancelled.first && reservation_cancelled.second.has_value()) { + EVLOG_debug << "Cancelled reservation with id " << reservation.reservation_id << " for evse id " + << reservation_cancelled.second.value() << " because a reservation with the same id was made"; } - if (state == ConnectorState::FAULTED) { - EVLOG_debug << "Rejecting reservation because connector is faulted"; - return types::reservation::ReservationResult::Faulted; + std::unique_lock lock(reservation_mutex); + + if (evse_id.has_value()) { + if (this->evse_reservations.count(evse_id.value()) > 0) { + // There already is a reservation for this evse. + EVLOG_debug << "Rejected reservation because there already is a reservation for this evse."; + return types::reservation::ReservationResult::Occupied; + } + + if (this->evses.count(evse_id.value()) == 0) { + // There is no evse with this evse id. + EVLOG_warning << "Rejected reservation because there is no evse with this evse id: " << evse_id.value(); + return types::reservation::ReservationResult::Rejected; + } + const types::evse_manager::ConnectorTypeEnum connector_type = + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown); + + // We have to return a valid state here. + // So if one or all connectors are occupied or reserved, return occupied. (H01.FR.11) + // If one or all are faulted, return faulted. (H01.FR.12) + // If one or all are unavailable, return unavailable. (H01.FR.13) + // It is not clear what to return if one is faulted, one occupied and one available so in that case the first + // in row is returned, which is occupied. + const types::reservation::ReservationResult evse_state = + this->get_evse_connector_state_reservation_result(evse_id.value(), this->evse_reservations); + const types::reservation::ReservationResult connector_state = + this->get_connector_availability_reservation_result(evse_id.value(), connector_type); + + if (!has_evse_connector_type(this->evses[evse_id.value()]->connectors, connector_type)) { + EVLOG_debug << "Rejected reservation because this evse (id: " << evse_id.value() + << ") does not have the requested connector type (" + << types::evse_manager::connector_type_enum_to_string(connector_type) << ")"; + return types::reservation::ReservationResult::Rejected; + } else if (evse_state != types::reservation::ReservationResult::Accepted) { + print_reservations_debug_info(reservation, evse_id, true); + EVLOG_debug << "Rejecting reservation because connector is not available"; + return evse_state; + } else if (connector_state != types::reservation::ReservationResult::Accepted) { + print_reservations_debug_info(reservation, evse_id, true); + return connector_state; + } else { + // Everything fine, continue. + if (global_reservations.empty()) { + set_reservation_timer(reservation, evse_id); + this->evse_reservations[evse_id.value()] = reservation; + EVLOG_info << "Created reservation for evse id " << evse_id.value() << ", connector type " + << types::evse_manager::connector_type_enum_to_string( + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)); + return types::reservation::ReservationResult::Accepted; + } + + // Make a copy of the evse specific reservations map so we can add this reservation to test if the + // reservation is possible. Only if it is, we add it to the 'member' map. + std::map evse_specific_reservations = this->evse_reservations; + evse_specific_reservations[evse_id.value()] = reservation; + + // Check if the reservations are possible with the added evse specific reservation. + if (!is_reservation_possible(std::nullopt, this->global_reservations, evse_specific_reservations)) { + print_reservations_debug_info(reservation, evse_id, true); + return get_reservation_evse_connector_state(connector_type); + } + + // Reservation is possible, add to evse specific reservations. + this->evse_reservations[evse_id.value()] = reservation; + EVLOG_info << "Created reservation for evse id " << evse_id.value() << ", connector type " + << types::evse_manager::connector_type_enum_to_string( + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)); + } + } else { + if (reservation.connector_type.has_value() && + !does_evse_connector_type_exist(reservation.connector_type.value())) { + EVLOG_info << "Can not make reservation because the connector type does not exist."; + return types::reservation::ReservationResult::Rejected; + } + + const types::evse_manager::ConnectorTypeEnum connector_type = + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown); + if (!is_reservation_possible(connector_type, this->global_reservations, this->evse_reservations)) { + print_reservations_debug_info(reservation, evse_id, true); + return get_reservation_evse_connector_state(connector_type); + } + + EVLOG_info << "Created reservation for connector type " + << types::evse_manager::connector_type_enum_to_string( + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)); + global_reservations.push_back(reservation); + store_reservations(); } - if (date::utc_clock::now() > Everest::Date::from_rfc3339(reservation.expiry_time)) { - EVLOG_debug << "Rejecting reservation because expiry_time is in the past"; + set_reservation_timer(reservation, evse_id); + + return types::reservation::ReservationResult::Accepted; +} + +void ReservationHandler::on_connector_state_changed(const ConnectorState connector_state, const uint32_t evse_id, + const uint32_t connector_id) { + if (connector_state == ConnectorState::AVAILABLE) { + // Nothing to cancel. + return; + } + + std::unique_lock lock(reservation_mutex); + std::unique_lock lock2(evse_mutex); + if (this->evses.count(static_cast(evse_id)) == 0) { + EVLOG_warning << "on_connector_state_changed: evse " << evse_id << " does not exist. This should not happen."; + return; + } + + auto& connectors = this->evses[evse_id]->connectors; + auto connector_it = std::find_if(connectors.begin(), connectors.end(), + [connector_id](const auto& connector) { return connector_id == connector.id; }); + + if (connector_it == connectors.end()) { + // Connector with specific connector id not found + EVLOG_warning << "Could not change connector state for connector id " << connector_id << " of evse " << evse_id + << ": connector id does not exist. This should not happen."; + return; + } + + const bool reservation_exists = evse_reservations.count(evse_id) != 0; + + if (reservation_exists && evse_reservations[evse_id].connector_type.has_value() && + (connector_it->type == evse_reservations[evse_id].connector_type.value() || + connector_it->type == types::evse_manager::ConnectorTypeEnum::Unknown || + evse_reservations[evse_id].connector_type.value() == types::evse_manager::ConnectorTypeEnum::Unknown)) { + cancel_reservation(evse_reservations[evse_id].reservation_id, true, + types::reservation::ReservationEndReason::Cancelled); + return; + } + + // Now we might have one connector less, let's check if all reservations are still possible now and if not, cancel + // the one(s) that can not be done anymore. + check_reservations_and_cancel_if_not_possible(); +} + +bool ReservationHandler::is_charging_possible(const uint32_t evse_id) { + std::unique_lock lock(reservation_mutex); + if (this->evse_reservations.count(evse_id) > 0) { + return false; + } + + if (this->evses.count(evse_id) == 0) { + // Not existing evse id + return false; + } + + std::map reservations = this->evse_reservations; + // We want to test if charging is possible on this evse id with the current reservations. For that, we do like it + // is a new reservation and check if that reservation is possible. If it is, we can charge on that evse. + types::reservation::Reservation r; + // It is a dummy reservation so the details are not important. + reservations[evse_id] = r; + return is_reservation_possible(std::nullopt, this->global_reservations, reservations); +} + +bool ReservationHandler::is_evse_reserved(const uint32_t evse_id) { + if (this->evse_reservations.count(evse_id) > 0) { + return true; + } + + return false; +} + +std::pair> +ReservationHandler::cancel_reservation(const int reservation_id, const bool execute_callback, + const types::reservation::ReservationEndReason reason) { + + std::pair> result; + + std::unique_lock lock(reservation_mutex); + + bool reservation_cancelled = false; + + { + std::unique_lock lk(this->timer_mutex); + auto reservation_id_timer_it = this->reservation_id_to_reservation_timeout_timer_map.find(reservation_id); + if (reservation_id_timer_it != this->reservation_id_to_reservation_timeout_timer_map.end()) { + reservation_id_timer_it->second->stop(); + this->reservation_id_to_reservation_timeout_timer_map.erase(reservation_id_timer_it); + reservation_cancelled = true; + result.first = true; + } + } + + if (!reservation_cancelled) { + result.first = false; + return result; + } + + EVLOG_info << "Cancel reservation with reservation id " << reservation_id; + + std::optional evse_id; + for (const auto& reservation : this->evse_reservations) { + if (reservation.second.reservation_id == reservation_id) { + evse_id = reservation.first; + } + } + + if (evse_id.has_value()) { + auto it = this->evse_reservations.find(evse_id.value()); + if (it != this->evse_reservations.end()) { + this->evse_reservations.erase(it); + } else { + EVLOG_warning << "Could not remove reservation with evse id " << evse_id.value() + << ": this should not happen"; + } + + } else { + // No evse, search in global reservations + const auto& it = std::find_if(this->global_reservations.begin(), this->global_reservations.end(), + [reservation_id](const types::reservation::Reservation& reservation) { + return reservation.reservation_id == reservation_id; + }); + + if (it != this->global_reservations.end()) { + this->global_reservations.erase(it); + } + } + + this->store_reservations(); + + if (execute_callback && this->reservation_cancelled_callback != nullptr) { + this->reservation_cancelled_callback(evse_id, reservation_id, reason, execute_callback); + } + + result.second = evse_id; + return result; +} + +void ReservationHandler::register_reservation_cancelled_callback( + const std::function& evse_id, const int32_t reservation_id, + const types::reservation::ReservationEndReason reason, + const bool send_reservation_update)>& callback) { + this->reservation_cancelled_callback = callback; +} + +void ReservationHandler::on_reservation_used(const int32_t reservation_id) { + const std::pair> cancelled = + this->cancel_reservation(reservation_id, true, types::reservation::ReservationEndReason::UsedToStartCharging); + if (cancelled.first) { + if (cancelled.second.has_value()) { + EVLOG_info << "Reservation (" << reservation_id << ") for evse#" << cancelled.second.value() + << " used and cancelled"; + } else { + EVLOG_info << "Reservation (" << reservation_id << ") without evse id used and cancelled"; + } + } else { + EVLOG_info << "Could not cancel reservation with reservation id " << reservation_id; + } +} + +std::optional ReservationHandler::matches_reserved_identifier(const std::string& id_token, + const std::optional evse_id, + std::optional parent_id_token) { + EVLOG_debug << "Matches reserved identifier for evse id " << (evse_id.has_value() ? evse_id.value() : 9999) + << " and id token " << id_token << " and parent id token " + << (parent_id_token.has_value() ? parent_id_token.value() : "-"); + + std::lock_guard lock(this->reservation_mutex); + + // Return true if id tokens match or parent id tokens exists and match. + if (evse_id.has_value()) { + if (this->evse_reservations.count(evse_id.value())) { + const types::reservation::Reservation& reservation = this->evse_reservations[evse_id.value()]; + if (reservation.id_token == id_token || + (parent_id_token.has_value() && reservation.parent_id_token.has_value() && + parent_id_token.value() == reservation.parent_id_token.value())) { + EVLOG_debug << "There is a reservation (" << reservation.reservation_id << ") for evse " + << evse_id.value() << " and the token matches"; + return reservation.reservation_id; + } else { + EVLOG_debug << "There is a reservation for evse id " << evse_id.value() << ", but token does not match"; + return std::nullopt; + } + } + } + + // If evse_id == 0 or there is no reservation found with the given evse id, search globally for reservation with + // this token. + for (const auto& reservation : global_reservations) { + if (reservation.id_token == id_token || + (parent_id_token.has_value() && reservation.parent_id_token.has_value() && + parent_id_token.value() == reservation.parent_id_token.value())) { + EVLOG_debug << "There is a reservation for the token, reservation id: " << reservation.reservation_id; + return reservation.reservation_id; + } + } + + EVLOG_debug << "No reservation found which matches the reserved identifier"; + return std::nullopt; +} + +bool ReservationHandler::has_reservation_parent_id(const std::optional evse_id) { + std::lock_guard lock(this->reservation_mutex); + + if (evse_id.has_value()) { + if (this->evses.count(evse_id.value()) == 0) { + // EVSE id does not exist. + return false; + } + + if (this->evse_reservations.count(evse_id.value())) { + return this->evse_reservations[evse_id.value()].parent_id_token.has_value(); + } + } + + // Check if one of the global reservations has a parent id. + for (const auto& reservation : this->global_reservations) { + if (reservation.parent_id_token.has_value()) { + return true; + } + } + + return false; +} + +bool ReservationHandler::has_evse_connector_type(const std::vector& evse_connectors, + const types::evse_manager::ConnectorTypeEnum connector_type) const { + if (connector_type == types::evse_manager::ConnectorTypeEnum::Unknown) { + return true; + } + + for (const auto& connector : evse_connectors) { + if (connector.type == types::evse_manager::ConnectorTypeEnum::Unknown || connector.type == connector_type) { + return true; + } + } + + return false; +} + +bool ReservationHandler::does_evse_connector_type_exist( + const types::evse_manager::ConnectorTypeEnum connector_type) const { + std::unique_lock lock(evse_mutex); + for (const auto& [evse_id, evse] : evses) { + if (has_evse_connector_type(evse->connectors, connector_type)) { + return true; + } + } + + return false; +} + +types::reservation::ReservationResult ReservationHandler::get_evse_connector_state_reservation_result( + const uint32_t evse_id, const std::map& evse_specific_reservations) { + if (evses.count(evse_id) == 0) { + EVLOG_warning << "Get evse state for evse " << evse_id + << " not possible: evse id does not exists. This should not happen."; return types::reservation::ReservationResult::Rejected; } - if (!is_reservable) { - EVLOG_debug << "Rejecting reservation because connector is not in state AVAILABLE"; + // Check if evse is available. + std::unique_lock lock(evse_mutex); + if (evses[evse_id]->plugged_in) { + return connector_state_to_reservation_result(ConnectorState::OCCUPIED); + } + + // If one connector is occupied, then the other connector can also not be used (one connector of an evse can be + // used at the same time). + std::unique_lock lock_evse(evses[evse_id]->event_mutex); + for (const auto& connector : evses[evse_id]->connectors) { + if (connector.get_state() == ConnectorState::OCCUPIED || + connector.get_state() == ConnectorState::FAULTED_OCCUPIED) { + return connector_state_to_reservation_result(connector.get_state()); + } + } + + lock.unlock(); + lock_evse.unlock(); + + // If evse is reserved, it is not available. + if (evse_specific_reservations.count(evse_id) > 0) { return types::reservation::ReservationResult::Occupied; } - if (!this->reservations.count(connector)) { - this->reservations[connector] = reservation; - std::lock_guard lk(this->timer_mutex); - this->connector_to_reservation_timeout_timer_map[connector]->at( - [this, reservation, connector]() { - EVLOG_info << "Reservation expired for connector#" << connector; - this->cancel_reservation(reservation.reservation_id, true); - }, - Everest::Date::from_rfc3339(reservation.expiry_time)); - return types::reservation::ReservationResult::Accepted; - } else { - EVLOG_debug << "Rejecting reservation because connector is already reserved"; + return types::reservation::ReservationResult::Accepted; +} + +types::reservation::ReservationResult ReservationHandler::get_connector_availability_reservation_result( + const uint32_t evse_id, const types::evse_manager::ConnectorTypeEnum connector_type) { + std::unique_lock lock(evse_mutex); + std::unique_lock lock_evse(evses[evse_id]->event_mutex); + if (evses.count(evse_id) == 0) { + EVLOG_warning << "Request if connector is available for evse id " << evse_id + << ", but evse id does not exist. This should not happen."; + return types::reservation::ReservationResult::Rejected; + } + + ConnectorState connector_state = ConnectorState::UNAVAILABLE; + + for (const auto& connector : evses[evse_id]->connectors) { + if ((connector.type == connector_type || connector.type == types::evse_manager::ConnectorTypeEnum::Unknown || + connector_type == types::evse_manager::ConnectorTypeEnum::Unknown)) { + if (connector.get_state() == ConnectorState::AVAILABLE) { + return types::reservation::ReservationResult::Accepted; + } else { + connector_state = get_new_connector_state(connector_state, connector.get_state()); + } + } + } + + return connector_state_to_reservation_result(connector_state); +} + +std::vector> ReservationHandler::get_all_possible_orders( + const std::vector& connectors) const { + + std::vector input_next = connectors; + std::vector input_prev = connectors; + std::vector> output; + + if (connectors.empty()) { + return output; + } + + // For next_permutation, the input must be ordered or it will stop halfway. So if it stops halafway, + // prev_permutation will find the others. + do { + output.push_back(input_next); + } while (std::next_permutation(input_next.begin(), input_next.end())); + + while (std::prev_permutation(input_prev.begin(), input_prev.end())) { + output.push_back(input_prev); + } + + return output; +} + +bool ReservationHandler::can_virtual_car_arrive( + const std::vector& used_evse_ids, + const std::vector& next_car_arrival_order, + const std::map& evse_specific_reservations) { + + bool is_possible = false; + + std::unique_lock lock(evse_mutex); + for (const auto& [evse_id, evse] : evses) { + // Check if there is a car already at this evse id. + if (std::find(used_evse_ids.begin(), used_evse_ids.end(), evse_id) != used_evse_ids.end()) { + continue; + } + + if (get_evse_connector_state_reservation_result(evse_id, evse_specific_reservations) == + types::reservation::ReservationResult::Accepted && + has_evse_connector_type(evse->connectors, next_car_arrival_order.at(0)) && + get_connector_availability_reservation_result(evse_id, next_car_arrival_order.at(0)) == + types::reservation::ReservationResult::Accepted) { + is_possible = true; + + std::vector next_used_evse_ids = used_evse_ids; + // Add evse id to list when we call the function recursively. + next_used_evse_ids.push_back(evse_id); + + // Check if this is the last. + if (next_car_arrival_order.size() == 1) { + // If this is the last and a car can arrive, then this combination is possible. + return true; + } + + // Call next level recursively. + // Remove connector type ('car') from list when we call the function recursively. + const std::vector next_arrival_order( + next_car_arrival_order.begin() + 1, next_car_arrival_order.end()); + + if (!this->can_virtual_car_arrive(next_used_evse_ids, next_arrival_order, evse_specific_reservations)) { + return false; + } + } + } + + return is_possible; +} + +bool ReservationHandler::is_reservation_possible( + const std::optional global_reservation_type, + const std::vector& reservations_no_evse, + const std::map& evse_specific_reservations) { + + std::vector types; + for (const auto& global_reservation : reservations_no_evse) { + types.push_back(global_reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)); + } + + if (global_reservation_type.has_value()) { + types.push_back(global_reservation_type.value()); + } + + // Check if the total amount of reservations is not more than the total amount of evse's. + if (types.size() + evse_specific_reservations.size() > this->evses.size()) { + return false; + } + + const std::vector> orders = get_all_possible_orders(types); + + std::unique_lock lock(evse_mutex); + for (const auto& o : orders) { + if (!this->can_virtual_car_arrive({}, o, evse_specific_reservations)) { + return false; + } + } + + return true; +} + +void ReservationHandler::set_reservation_timer(const types::reservation::Reservation& reservation, + const std::optional evse_id) { + std::lock_guard lk(this->timer_mutex); + this->reservation_id_to_reservation_timeout_timer_map[reservation.reservation_id] = + std::make_unique(&this->io_service); + + this->reservation_id_to_reservation_timeout_timer_map[reservation.reservation_id]->at( + [this, reservation, evse_id]() { + if (evse_id.has_value()) { + EVLOG_info << "Reservation expired for evse #" << evse_id.value() + << " (reservation id: " << reservation.reservation_id << ")"; + } else { + EVLOG_info << "Reservation expired for reservation id " << reservation.reservation_id; + } + + this->cancel_reservation(reservation.reservation_id, true, + types::reservation::ReservationEndReason::Expired); + }, + Everest::Date::from_rfc3339(reservation.expiry_time)); +} + +std::vector ReservationHandler::get_all_evses_with_connector_type( + const types::evse_manager::ConnectorTypeEnum connector_type) const { + std::unique_lock lock(evse_mutex); + std::vector result; + for (const auto& evse : this->evses) { + if (this->has_evse_connector_type(evse.second->connectors, connector_type)) { + result.push_back(evse.second.get()); + } + } + + return result; +} + +ConnectorState ReservationHandler::get_new_connector_state(ConnectorState current_state, + const ConnectorState new_state) const { + if (new_state == ConnectorState::OCCUPIED) { + return ConnectorState::OCCUPIED; + } + + if (new_state > current_state) { + if (new_state > ConnectorState::OCCUPIED) { + if (new_state == ConnectorState::FAULTED_OCCUPIED) { + current_state = ConnectorState::OCCUPIED; + } else if (new_state == ConnectorState::UNAVAILABLE_FAULTED) { + if (current_state != ConnectorState::OCCUPIED) { + current_state = ConnectorState::FAULTED; + } + } + } else { + current_state = new_state; + } + } + + return current_state; +} + +types::reservation::ReservationResult ReservationHandler::get_reservation_evse_connector_state( + const types::evse_manager::ConnectorTypeEnum connector_type) const { + // If at least one connector is occupied, return occupied. + if (!global_reservations.empty() || !(evse_reservations.empty())) { return types::reservation::ReservationResult::Occupied; } + + bool found_state = false; + + std::unique_lock lock(evse_mutex); + ConnectorState state = ConnectorState::UNAVAILABLE; + + for (const auto& [evse_id, evse] : evses) { + if (evse->plugged_in) { + // Overwrite state if we found a connector that was not available (if needed). + state = get_new_connector_state(state, ConnectorState::OCCUPIED); + found_state = true; + } + } + + if (!found_state) { + const std::vector evses_with_connector_type = + this->get_all_evses_with_connector_type(connector_type); + if (evses_with_connector_type.empty()) { + // This should not happen because then it should have been rejected before already somewhere in the + // code... + return types::reservation::ReservationResult::Rejected; + } + + // Get all evse's with this specific connector type and check the connectors availability states. + for (const auto& evse : evses_with_connector_type) { + std::unique_lock lock(evse->event_mutex); + for (const auto& connector : evse->connectors) { + if (connector.type != connector_type && + connector.type != types::evse_manager::ConnectorTypeEnum::Unknown && + connector_type != types::evse_manager::ConnectorTypeEnum::Unknown) { + continue; + } + + if (connector.get_state() != ConnectorState::AVAILABLE) { + state = get_new_connector_state(state, connector.get_state()); + } + } + } + } + + return connector_state_to_reservation_result(state); } -int ReservationHandler::cancel_reservation(int reservation_id, bool execute_callback) { - std::lock_guard lk(this->reservation_mutex); - int connector = -1; - for (const auto& reservation : this->reservations) { - if (reservation.second.reservation_id == reservation_id) { - connector = reservation.first; +void ReservationHandler::check_reservations_and_cancel_if_not_possible() { + std::unique_lock lock(reservation_mutex); + + std::vector reservations_to_cancel; + std::map evse_specific_reservations; + std::vector reservations_no_evse; + + for (const auto& [evse_id, reservation] : this->evse_reservations) { + evse_specific_reservations[evse_id] = reservation; + if (!is_reservation_possible(std::nullopt, reservations_no_evse, evse_specific_reservations)) { + reservations_to_cancel.push_back(reservation.reservation_id); + evse_specific_reservations.erase(evse_id); } } - if (connector != -1) { - std::lock_guard lk(this->timer_mutex); - this->connector_to_reservation_timeout_timer_map[connector]->stop(); - auto it = this->reservations.find(connector); - this->reservations.erase(it); - if (execute_callback) { - this->reservation_cancelled_callback(connector); + + for (const auto& reservation : this->global_reservations) { + if (is_reservation_possible(reservation.connector_type, reservations_no_evse, evse_specific_reservations)) { + reservations_no_evse.push_back(reservation); + } else { + reservations_to_cancel.push_back(reservation.reservation_id); } } - return connector; + + for (const int32_t reservation_id : reservations_to_cancel) { + this->cancel_reservation(reservation_id, true, types::reservation::ReservationEndReason::Cancelled); + } +} + +void ReservationHandler::store_reservations() { + if (this->store == nullptr) { + return; + } + + Array reservations = json::array(); + for (const auto& reservation : this->evse_reservations) { + + json r = json::object({{"evse_id", reservation.first}, {"reservation", reservation.second}}); + reservations.push_back(r); + } + + for (const auto& reservation : this->global_reservations) { + json r = json::object({{"reservation", reservation}}); + reservations.push_back(r); + } + + if (!reservations.empty()) { + this->store->call_store(this->kvs_store_key_id, reservations); + } } -void ReservationHandler::on_reservation_used(int connector) { - if (this->cancel_reservation(this->reservations[connector].reservation_id, false)) { - EVLOG_info << "Reservation for connector#" << connector << " used and cancelled"; +void ReservationHandler::print_reservations_debug_info(const types::reservation::Reservation& reservation, + const std::optional evse_id, + const bool reservation_failed) { + std::string reservation_information; + if (reservation_failed) { + reservation_information = "Reservation not possible"; } else { - EVLOG_warning - << "On reservation used called when no reservation for this connector was present. This should not happen"; + reservation_information = "New reservation"; + } + EVLOG_debug << reservation_information + << ". Evse id: " << (evse_id.has_value() ? std::to_string(evse_id.value()) : "no evse id") + << ", connector type: " + << (reservation.connector_type.has_value() + ? types::evse_manager::connector_type_enum_to_string(reservation.connector_type.value()) + : "no connector type given"); + std::string evse_info; + for (const auto& evse : this->evses) { + evse_info += "- " + std::to_string(evse.first) + ":\n"; + for (const auto& connector : evse.second->connectors) { + evse_info += "--- " + std::to_string(connector.id) + " " + + types::evse_manager::connector_type_enum_to_string(connector.type) + + ", available: " + (connector.get_state() == ConnectorState::AVAILABLE ? "yes" : "no") + "\n"; + } + } + std::string reservation_info; + for (const auto& evse_reservation : this->evse_reservations) { + reservation_info += + "- evse " + std::to_string(evse_reservation.first) + ": " + + types::evse_manager::connector_type_enum_to_string( + evse_reservation.second.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)) + + "\n"; } + + for (const auto& reservation : this->global_reservations) { + reservation_info += "- global : " + + types::evse_manager::connector_type_enum_to_string( + reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown)) + + "\n"; + } + + EVLOG_debug << "Current evse's and states: \n" << evse_info; + EVLOG_debug << "Current reservations: \n" << reservation_info; } -void ReservationHandler::register_reservation_cancelled_callback( - const std::function& callback) { - this->reservation_cancelled_callback = callback; +static types::reservation::ReservationResult +connector_state_to_reservation_result(const ConnectorState connector_state) { + switch (connector_state) { + case ConnectorState::AVAILABLE: + return types::reservation::ReservationResult::Accepted; + case ConnectorState::UNAVAILABLE: + return types::reservation::ReservationResult::Unavailable; + case ConnectorState::FAULTED: + case ConnectorState::UNAVAILABLE_FAULTED: + case ConnectorState::FAULTED_OCCUPIED: + return types::reservation::ReservationResult::Faulted; + case ConnectorState::OCCUPIED: + return types::reservation::ReservationResult::Occupied; + } + + return types::reservation::ReservationResult::Rejected; } } // namespace module diff --git a/modules/Auth/main/authImpl.hpp b/modules/Auth/main/authImpl.hpp index 2db7bd2fa..da04d0d73 100644 --- a/modules/Auth/main/authImpl.hpp +++ b/modules/Auth/main/authImpl.hpp @@ -25,7 +25,8 @@ class authImpl : public authImplBase { public: authImpl() = delete; authImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - authImplBase(ev, "main"), mod(mod), config(config){}; + authImplBase(ev, "main"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/Auth/manifest.yaml b/modules/Auth/manifest.yaml index 9cd7c9f63..55cc7a45f 100644 --- a/modules/Auth/manifest.yaml +++ b/modules/Auth/manifest.yaml @@ -73,6 +73,10 @@ requires: interface: evse_manager min_connections: 1 max_connections: 128 + kvs: + interface: kvs + min_connections: 0 + max_connections: 1 metadata: license: https://opensource.org/licenses/Apache-2.0 authors: diff --git a/modules/Auth/reservation/reservationImpl.cpp b/modules/Auth/reservation/reservationImpl.cpp index 30a445252..3bc52c335 100644 --- a/modules/Auth/reservation/reservationImpl.cpp +++ b/modules/Auth/reservation/reservationImpl.cpp @@ -12,24 +12,37 @@ void reservationImpl::init() { void reservationImpl::ready() { } -types::reservation::ReservationResult -reservationImpl::handle_reserve_now(int& connector_id, types::reservation::Reservation& reservation) { +types::reservation::ReservationResult reservationImpl::handle_reserve_now(types::reservation::Reservation& request) { // your code for cmd reserve_now goes here - const auto reservation_result = this->mod->auth_handler->handle_reservation(connector_id, reservation); + EVLOG_debug << "Handle reservation for evse id " << (request.evse_id.has_value() ? request.evse_id.value() : -1); + + const auto reservation_result = this->mod->auth_handler->handle_reservation(request); if (reservation_result == ReservationResult::Accepted) { - this->mod->auth_handler->call_reserved(connector_id, reservation.reservation_id); + if (!this->mod->auth_handler->call_reserved(request.reservation_id, request.evse_id)) { + return ReservationResult::Rejected; + } } return reservation_result; }; bool reservationImpl::handle_cancel_reservation(int& reservation_id) { - const auto connector = this->mod->auth_handler->handle_cancel_reservation(reservation_id); - if (connector != -1) { - this->mod->auth_handler->call_reservation_cancelled(connector); + const auto reservation_cancelled = this->mod->auth_handler->handle_cancel_reservation(reservation_id); + if (reservation_cancelled.first) { + // Call reservation cancelled. This comes from outside, so we don't send the status update (otherwise this is + // sent to OCPP and that is not according to specification). + this->mod->auth_handler->call_reservation_cancelled(reservation_id, ReservationEndReason::Cancelled, + reservation_cancelled.second, false); return true; } + return false; +} + +types::reservation::ReservationCheckStatus +reservationImpl::handle_exists_reservation(types::reservation::ReservationCheck& request) { + return this->mod->auth_handler->handle_reservation_exists(request.id_token, request.evse_id, + request.group_id_token); }; } // namespace reservation diff --git a/modules/Auth/reservation/reservationImpl.hpp b/modules/Auth/reservation/reservationImpl.hpp index bb30a19a7..e66964b05 100644 --- a/modules/Auth/reservation/reservationImpl.hpp +++ b/modules/Auth/reservation/reservationImpl.hpp @@ -25,7 +25,8 @@ class reservationImpl : public reservationImplBase { public: reservationImpl() = delete; reservationImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - reservationImplBase(ev, "reservation"), mod(mod), config(config){}; + reservationImplBase(ev, "reservation"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here @@ -33,9 +34,10 @@ class reservationImpl : public reservationImplBase { protected: // command handler functions (virtual) - virtual types::reservation::ReservationResult - handle_reserve_now(int& connector_id, types::reservation::Reservation& reservation) override; + virtual types::reservation::ReservationResult handle_reserve_now(types::reservation::Reservation& request) override; virtual bool handle_cancel_reservation(int& reservation_id) override; + virtual types::reservation::ReservationCheckStatus + handle_exists_reservation(types::reservation::ReservationCheck& request) override; // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 // insert your protected definitions here diff --git a/modules/Auth/tests/CMakeLists.txt b/modules/Auth/tests/CMakeLists.txt index 135fd983d..94b400148 100644 --- a/modules/Auth/tests/CMakeLists.txt +++ b/modules/Auth/tests/CMakeLists.txt @@ -1,9 +1,19 @@ set(TEST_TARGET_NAME ${PROJECT_NAME}_auth_tests) -add_executable(${TEST_TARGET_NAME} auth_tests.cpp) + +set(TEST_SOURCES ${PROJECT_SOURCE_DIR}/modules/Auth/lib/ReservationHandler.cpp + ${PROJECT_SOURCE_DIR}/modules/Auth/lib/AuthHandler.cpp + ${PROJECT_SOURCE_DIR}/modules/Auth/lib/Connector.cpp + ${PROJECT_SOURCE_DIR}/modules/Auth/lib/ConnectorStateMachine.cpp) + +add_executable(${TEST_TARGET_NAME} auth_tests.cpp reservation_tests.cpp ${TEST_SOURCES}) + +message("Current source dir: ${CMAKE_CURRENT_SOURCE_DIR}") set(INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/stubs" "${PROJECT_SOURCE_DIR}/modules/Auth/include" - "${PROJECT_SOURCE_DIR}/modules/Auth/tests") + "${PROJECT_SOURCE_DIR}/modules/Auth/tests" +) get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) @@ -20,7 +30,6 @@ target_link_libraries(${TEST_TARGET_NAME} PRIVATE everest::log everest::framework pthread - auth_handler nlohmann_json::nlohmann_json date::date date::date-tz diff --git a/modules/Auth/tests/auth_tests.cpp b/modules/Auth/tests/auth_tests.cpp index da74b9264..3b9482009 100644 --- a/modules/Auth/tests/auth_tests.cpp +++ b/modules/Auth/tests/auth_tests.cpp @@ -16,6 +16,8 @@ using ::testing::Field; using ::testing::MockFunction; using ::testing::StrictMock; +class kvsIntf; + namespace types { namespace authorization { @@ -84,8 +86,9 @@ class AuthTest : public ::testing::Test { std::vector evse_indices{0, 1}; this->auth_receiver = std::make_unique(evse_indices); - this->auth_handler = - std::make_unique(SelectionAlgorithm::PlugEvents, CONNECTION_TIMEOUT, false, false); + const std::string id = "auth_handler_test_id"; + this->auth_handler = std::make_unique(SelectionAlgorithm::PlugEvents, CONNECTION_TIMEOUT, false, + false, id, nullptr); this->auth_handler->register_notify_evse_callback([this](const int evse_index, const ProvidedIdToken& provided_token, @@ -132,15 +135,20 @@ class AuthTest : public ::testing::Test { return validation_results; }); - this->auth_handler->register_reservation_cancelled_callback([this](const int32_t evse_index) { - EVLOG_info << "Signaling reservating cancelled to evse#" << evse_index; - }); + this->auth_handler->register_reservation_cancelled_callback( + [](const std::optional evse_index, const int32_t reservation_id, const ReservationEndReason reason, + const bool send_reservation_update) { + EVLOG_info << "Signaling reservating cancelled to evse#" + << (evse_index.has_value() ? evse_index.value() : 0); + }); this->auth_handler->register_publish_token_validation_status_callback( mock_publish_token_validation_status_callback.AsStdFunction()); - this->auth_handler->init_connector(1, 0); - this->auth_handler->init_connector(2, 1); + this->auth_handler->init_evse(1, 0, {Connector(1, types::evse_manager::ConnectorTypeEnum::cCCS2)}); + this->auth_handler->init_evse(2, 1, + {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2), + Connector(2, types::evse_manager::ConnectorTypeEnum::cCCS2)}); } void TearDown() override { @@ -458,15 +466,16 @@ TEST_F(AuthTest, test_two_plugins_with_invalid_rfid) { /// \brief Test if state permanent fault leads to not provide authorization TEST_F(AuthTest, test_faulted_state) { - TokenHandlingResult result1; TokenHandlingResult result2; - std::thread t1([this]() { this->auth_handler->handle_permanent_fault_raised(1); }); - std::thread t2([this]() { this->auth_handler->handle_permanent_fault_raised(2); }); + std::thread t1([this]() { this->auth_handler->handle_permanent_fault_raised(1, 1); }); + std::thread t2([this]() { this->auth_handler->handle_permanent_fault_raised(2, 1); }); + std::thread t3([this]() { this->auth_handler->handle_permanent_fault_raised(2, 2); }); t1.join(); t2.join(); + t3.join(); std::vector connectors{1, 2}; ProvidedIdToken provided_token_1 = get_provided_token(VALID_TOKEN_1, connectors); @@ -482,11 +491,11 @@ TEST_F(AuthTest, test_faulted_state) { EXPECT_CALL(mock_publish_token_validation_status_callback, Call(Field(&ProvidedIdToken::id_token, provided_token_2.id_token), TokenValidationStatus::Rejected)); - std::thread t3([this, provided_token_1, &result1]() { result1 = this->auth_handler->on_token(provided_token_1); }); - std::thread t4([this, provided_token_2, &result2]() { result2 = this->auth_handler->on_token(provided_token_2); }); + std::thread t4([this, provided_token_1, &result1]() { result1 = this->auth_handler->on_token(provided_token_1); }); + std::thread t5([this, provided_token_2, &result2]() { result2 = this->auth_handler->on_token(provided_token_2); }); - t3.join(); t4.join(); + t5.join(); ASSERT_TRUE(result1 == TokenHandlingResult::NO_CONNECTOR_AVAILABLE); ASSERT_TRUE(result2 == TokenHandlingResult::NO_CONNECTOR_AVAILABLE); @@ -662,7 +671,10 @@ TEST_F(AuthTest, test_parent_id_finish_because_no_available_connector) { SessionEvent session_event_1 = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); std::thread t1([this, session_event_1]() { this->auth_handler->handle_session_event(1, session_event_1); }); - std::thread t2([this]() { this->auth_handler->handle_permanent_fault_raised(2); }); + std::thread t2([this]() { + this->auth_handler->handle_permanent_fault_raised(2, 1); + this->auth_handler->handle_permanent_fault_raised(2, 2); + }); std::vector connectors{1, 2}; ProvidedIdToken provided_token_1 = get_provided_token(VALID_TOKEN_1, connectors); @@ -705,14 +717,70 @@ TEST_F(AuthTest, test_parent_id_finish_because_no_available_connector) { ASSERT_FALSE(this->auth_receiver->get_authorization(1)); } +/// \brief Test if transaction doesnt finish with parent_id when prioritize_authorization_over_stopping_transaction is +/// true. Instead: Authorization should be given to connector#2 +TEST_F(AuthTest, test_parent_id_finish_because_no_available_connector_2) { + // Same test as above, but now the other evse is set to faulted. + TokenHandlingResult result; + + this->auth_handler->set_prioritize_authorization_over_stopping_transaction(true); + + SessionEvent session_event_1 = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + + std::thread t1([this, session_event_1]() { this->auth_handler->handle_session_event(2, session_event_1); }); + std::thread t2([this]() { this->auth_handler->handle_permanent_fault_raised(1, 1); }); + + std::vector connectors{1, 2}; + ProvidedIdToken provided_token_1 = get_provided_token(VALID_TOKEN_1, connectors); + ProvidedIdToken provided_token_2 = get_provided_token(VALID_TOKEN_3, connectors); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_1.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_2.id_token), TokenValidationStatus::Processing)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_1.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_2.id_token), TokenValidationStatus::Accepted)); + + // swipe VALID_TOKEN_1 + std::thread t3([this, provided_token_1, &result]() { result = this->auth_handler->on_token(provided_token_1); }); + + t1.join(); + t2.join(); + t3.join(); + + ASSERT_TRUE(result == TokenHandlingResult::ACCEPTED); + + SessionEvent session_event_3 = get_transaction_started_event(provided_token_1); + std::thread t4([this, session_event_3]() { this->auth_handler->handle_session_event(2, session_event_3); }); + + t4.join(); + + ASSERT_TRUE(this->auth_receiver->get_authorization(1)); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + + // swipe VALID_TOKEN_3. This does finish transaction because no connector is available + std::thread t5([this, provided_token_2, &result]() { result = this->auth_handler->on_token(provided_token_2); }); + + t5.join(); + + ASSERT_TRUE(result == TokenHandlingResult::USED_TO_STOP_TRANSACTION); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + /// \brief Test if a reservation can be placed TEST_F(AuthTest, test_reservation) { Reservation reservation; + reservation.evse_id = 1; reservation.id_token = VALID_TOKEN_1; reservation.reservation_id = 1; + reservation.connector_type = types::evse_manager::ConnectorTypeEnum::cCCS2; reservation.expiry_time = Everest::Date::to_rfc3339((date::utc_clock::now() + std::chrono::hours(1))); - const auto reservation_result = this->auth_handler->handle_reservation(1, reservation); + const auto reservation_result = this->auth_handler->handle_reservation(reservation); ASSERT_EQ(reservation_result, ReservationResult::Accepted); } @@ -720,11 +788,13 @@ TEST_F(AuthTest, test_reservation) { /// \brief Test if a reservation cannot be placed if expiry_time is in the past TEST_F(AuthTest, test_reservation_in_past) { Reservation reservation; + reservation.evse_id = 1; reservation.id_token = VALID_TOKEN_1; reservation.reservation_id = 1; + reservation.connector_type = types::evse_manager::ConnectorTypeEnum::cCCS2; reservation.expiry_time = Everest::Date::to_rfc3339((date::utc_clock::now() - std::chrono::hours(1))); - const auto reservation_result = this->auth_handler->handle_reservation(1, reservation); + const auto reservation_result = this->auth_handler->handle_reservation(reservation); ASSERT_EQ(reservation_result, ReservationResult::Rejected); } @@ -735,11 +805,13 @@ TEST_F(AuthTest, test_reservation_with_authorization) { TokenHandlingResult result; Reservation reservation; + reservation.evse_id = 1; reservation.id_token = VALID_TOKEN_2; reservation.reservation_id = 1; + reservation.connector_type = types::evse_manager::ConnectorTypeEnum::cCCS2; reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::hours(1)); - const auto reservation_result = this->auth_handler->handle_reservation(1, reservation); + const auto reservation_result = this->auth_handler->handle_reservation(reservation); ASSERT_EQ(reservation_result, ReservationResult::Accepted); @@ -776,7 +848,7 @@ TEST_F(AuthTest, test_reservation_with_authorization) { std::thread t3([this, provided_token_1, &result]() { result = this->auth_handler->on_token(provided_token_1); }); t3.join(); - ASSERT_TRUE(result == TokenHandlingResult::REJECTED); + ASSERT_EQ(result, TokenHandlingResult::REJECTED); ASSERT_FALSE(this->auth_receiver->get_authorization(0)); ASSERT_FALSE(this->auth_receiver->get_authorization(1)); @@ -784,11 +856,111 @@ TEST_F(AuthTest, test_reservation_with_authorization) { std::thread t4([this, provided_token_2, &result]() { result = this->auth_handler->on_token(provided_token_2); }); t4.join(); - ASSERT_TRUE(result == TokenHandlingResult::ACCEPTED); + ASSERT_EQ(result, TokenHandlingResult::ACCEPTED); ASSERT_TRUE(this->auth_receiver->get_authorization(0)); ASSERT_FALSE(this->auth_receiver->get_authorization(1)); } +/// \brief Test if a token that is not reserved gets rejected when it is not possible to charge because of global +/// reservations. +TEST_F(AuthTest, test_reservation_with_authorization_global_reservations) { + TokenHandlingResult result; + + Reservation reservation; + reservation.id_token = VALID_TOKEN_2; + reservation.reservation_id = 1; + reservation.connector_type = types::evse_manager::ConnectorTypeEnum::sType2; + reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::hours(1)); + + const auto reservation_result = this->auth_handler->handle_reservation(reservation); + + ASSERT_EQ(reservation_result, ReservationResult::Accepted); + + SessionEvent session_event_1; + session_event_1.event = SessionEventEnum::ReservationStart; + std::thread t1([this, session_event_1]() { this->auth_handler->handle_session_event(2, session_event_1); }); + + t1.join(); + + std::vector connectors{1, 2}; + ProvidedIdToken provided_token_1 = get_provided_token(VALID_TOKEN_1, connectors); + + // In general the token gets accepted but the connector that was picked up by the user is the only one that has + // the correct connector for the reservation so it can not be used as it has to be available for the one who + // reserved it. + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_1.id_token), TokenValidationStatus::Processing)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_1.id_token), TokenValidationStatus::Accepted)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_1.id_token), TokenValidationStatus::Rejected)); + + // this token is not valid for the reservation + std::thread t2([this, provided_token_1, &result]() { result = this->auth_handler->on_token(provided_token_1); }); + SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::Authorized); + std::thread t3([this, session_event]() { this->auth_handler->handle_session_event(2, session_event); }); + + t2.join(); + t3.join(); + + ASSERT_EQ(result, TokenHandlingResult::REJECTED); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + +/// \brief Test if a token that is not reserved gets rejected when it is not possible to charge because of global +/// reservations. +TEST_F(AuthTest, test_reservation_with_authorization_global_reservations_2) { + TokenHandlingResult result; + + // Make two global reservations. + + Reservation reservation; + reservation.id_token = VALID_TOKEN_2; + reservation.reservation_id = 1; + reservation.connector_type = types::evse_manager::ConnectorTypeEnum::cCCS2; + reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::hours(1)); + + const auto reservation_result = this->auth_handler->handle_reservation(reservation); + + ASSERT_EQ(reservation_result, ReservationResult::Accepted); + + reservation.reservation_id = 2; + reservation.id_token = VALID_TOKEN_1; + const auto reservation_result2 = this->auth_handler->handle_reservation(reservation); + ASSERT_EQ(reservation_result2, ReservationResult::Accepted); + + SessionEvent session_event_1; + session_event_1.event = SessionEventEnum::ReservationStart; + std::thread t1([this, session_event_1]() { this->auth_handler->handle_session_event(2, session_event_1); }); + + t1.join(); + + std::vector connectors{1, 2}; + ProvidedIdToken provided_token_3 = get_provided_token(VALID_TOKEN_3, connectors); + + // There are two global reservations and two evse's, so no evse is available. + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_3.id_token), TokenValidationStatus::Processing)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token_3.id_token), TokenValidationStatus::Rejected)); + + // this token is not valid for the reservation + std::thread t2([this, provided_token_3, &result]() { result = this->auth_handler->on_token(provided_token_3); }); + SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::Authorized); + std::thread t3([this, session_event]() { this->auth_handler->handle_session_event(2, session_event); }); + + t2.join(); + t3.join(); + + ASSERT_EQ(result, TokenHandlingResult::NO_CONNECTOR_AVAILABLE); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + /// \brief Test complete happy event flow of a session TEST_F(AuthTest, test_complete_event_flow) { @@ -856,12 +1028,13 @@ TEST_F(AuthTest, test_reservation_with_parent_id_tag) { TokenHandlingResult result; Reservation reservation; + reservation.evse_id = 1; reservation.id_token = VALID_TOKEN_1; reservation.reservation_id = 1; reservation.parent_id_token.emplace(PARENT_ID_TOKEN); reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::hours(1)); - const auto reservation_result = this->auth_handler->handle_reservation(1, reservation); + const auto reservation_result = this->auth_handler->handle_reservation(reservation); ASSERT_EQ(reservation_result, ReservationResult::Accepted); @@ -1209,7 +1382,6 @@ TEST_F(AuthTest, test_token_timed_out) { // To get select_connector to wait for a plug-in event, we must provide more then one connector here, since if we // provide only 1, select_connector would just return the single connector. std::vector connectors{1, 2}; - ProvidedIdToken provided_token = get_provided_token(VALID_TOKEN_1, connectors); EXPECT_CALL(mock_publish_token_validation_status_callback, diff --git a/modules/Auth/tests/reservation_tests.cpp b/modules/Auth/tests/reservation_tests.cpp new file mode 100644 index 000000000..25e9339d0 --- /dev/null +++ b/modules/Auth/tests/reservation_tests.cpp @@ -0,0 +1,1278 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include +#include + +#include + +#define private public +// Make 'ReservationHandler.hpp privates public to test a helper function 'get_all_possible_orders'. +#include "ReservationHandler.hpp" +#undef private + +using testing::_; +using testing::MockFunction; +using testing::Return; +using testing::SaveArg; + +using namespace types::reservation; + +namespace module { +class ReservationHandlerTest : public ::testing::Test { +private: + uint32_t reservation_id = 0; + +protected: + Reservation create_reservation(const types::evse_manager::ConnectorTypeEnum connector_type) { + return Reservation{static_cast(reservation_id), + "TOKEN_" + std::to_string(reservation_id++), + Everest::Date::to_rfc3339((date::utc_clock::now()) + std::chrono::hours(1)), + std::nullopt, + std::nullopt, + connector_type}; + } + + void add_connector(const int32_t evse_id, const uint32_t connector_id, + const types::evse_manager::ConnectorTypeEnum type, + std::map>& evses) { + if (evses.count(evse_id) > 0) { + evses[evse_id]->connectors.push_back(Connector{static_cast(connector_id), type}); + } else { + evses[evse_id] = std::make_unique(evse_id, evse_id - 1, connector_id, type); + } + } + + kvsIntf kvs; + std::map> evses; + std::recursive_mutex mutex; + ReservationHandler r{evses, mutex, "reservation_kvs", &kvs}; +}; + +TEST_F(ReservationHandlerTest, global_reservation_scenario_01) { + // Test global reservations (not bound to specific evse id). 3 EVSE's, one with cCCS2, two with cCCS2 and cType2. + // Three cCCS2 reservations should be accepted. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_02) { + // Test global reservations (not bound to specific evse id). 3 EVSE's, one with cCCS2, two with cCCS2 and cType2. + // One cCCS2 and one cType2 reservation should be accepted, but another reservation can not be made. Because if + // there would be two cCCS2 and one cType2, it is possible that first two cCCS2 type cars arrive, charge at the + // two combined charging stations and when the cType2 car arrives, there is no charging station with this connector + // available anymore. + + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_03) { + // Test global reservations (not bound to specific evse id). 3 EVSE's, one with cCCS2, two with cCCS2 and cType2. + // Two cType2 reservations should be accepted, but a third reservation is not accepted, because it is not guaranteed + // that in all circumstances a charger is available (for example cCCS2 goes to evse 2, cType2 goes to connector + // 1 and the second cType2 arrives but no charger is available anymore). + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_04) { + // Test global reservations (not bound to specific evse id). 3 EVSE's, one with cCCS2, two with cCCS2 and cType2. + // A cCCS2 and cType2 reservation should be accepted, because it does not matter in which order they arrive, there + // is always an evse available for the other one. But a cType2 as third reservation is not possible. Imagine the + // first car that arrives is cCCS2 and charges at evse 4 or 7, the second car can only put it at 4 or 7, then + // the third car that arrives (cType2) does not have an EVSE for his type available. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(4, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(4, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(7, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(7, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_05) { + // Test global reservations (not bound to specific evse id). 4 EVSE's, three with cCCS2, one with cCCS2 and cType2. + // When a cCCS2 reservation is made, cType2 can not make a reservation anymore, because it is possible that when + // the cCCS2 car first arrives, there is no EVSE available for the cType2 car anymore (evse 2). + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(3, 5, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_06) { + // Test global reservations (not bound to specific evse id). 3 EVSE's, two with cCCS2, one with cCCS2 and cType2. + // Only one cType2 reservation can be made and nothing else, also no cCCS2 reservation (because when the cCCS2 car + // arives first and puts it on connector 2, the cType2 that arrives second does not have an EVSE available anymore). + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_07) { + // Test global reservations (not bound to specific evse id). 1 EVSE only. Unknown is accepted, a type that is not + // available is rejected. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::sType3, evses); + + // There is no cType2 connector on this evse. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Rejected); + // Unknown is accepted + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_08) { + // Test global reservations (not bound to specific evse id). 3 EVSE's with all cCCS2 and cType2 connectors. + // Unknown and cCCS2 reservations are accepted, max 3 in total. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_09) { + // Test global reservations (not bound to specific evse id). 3 EVSE's with all cCCS2 and cType2 connectors. + // Unknown, cType2 and cCCS2 reservations are accepted, max 3 in total. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_10) { + // Test global reservations (not bound to specific evse id). 3 EVSE's with all two 'Unknown' connectors. + // Three reservations are accepted in total, it does not matter what connector types they have. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + add_connector(5, 0, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + add_connector(5, 1, types::evse_manager::ConnectorTypeEnum::Unknown, evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Other3Ph)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_11) { + // Test global reservations (not bound to specific evse id). 3 EVSE's with only one cCCS2 connector each. + // In total three reservations are accepted with the correct type (cCCS2 or Unknown). + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::Unknown)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Rejected); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_scenario_12) { + // Test global reservations (not bound to specific evse id). One EVSE with cCCS2 and cType2, one with cType2 and + // cTesla, one with cTesla and cCCS2. + // Only two reservations can be accepted, for the third there is no guarantee there is always place to charge in all + // orders of arrival of the different cars. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cTesla, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cTesla, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType1)), + ReservationResult::Rejected); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cTesla)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, get_all_possible_orders) { + using namespace types::evse_manager; + std::vector connectors; + connectors.push_back(ConnectorTypeEnum::cCCS2); + + std::vector> result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, std::vector>({{ConnectorTypeEnum::cCCS2}})); + + connectors.push_back(ConnectorTypeEnum::cCCS2); + result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, + std::vector>({{ConnectorTypeEnum::cCCS2, ConnectorTypeEnum::cCCS2}})); + + connectors.push_back(ConnectorTypeEnum::cCCS1); + result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, std::vector>( + {{ConnectorTypeEnum::cCCS2, ConnectorTypeEnum::cCCS2, ConnectorTypeEnum::cCCS1}, + {ConnectorTypeEnum::cCCS2, ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::cCCS2}, + {ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::cCCS2, ConnectorTypeEnum::cCCS2}})); +} + +TEST_F(ReservationHandlerTest, get_all_possible_orders2) { + using namespace types::evse_manager; + std::vector connectors; + connectors.push_back(ConnectorTypeEnum::cType1); + + std::vector> result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, std::vector>({{ConnectorTypeEnum::cType1}})); + + connectors.push_back(ConnectorTypeEnum::Pan); + result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, + std::vector>({{ConnectorTypeEnum::cType1, ConnectorTypeEnum::Pan}, + {ConnectorTypeEnum::Pan, ConnectorTypeEnum::cType1}})); + + connectors.push_back(ConnectorTypeEnum::cCCS1); + result = r.get_all_possible_orders(connectors); + EXPECT_EQ(result, std::vector>( + {{ConnectorTypeEnum::cType1, ConnectorTypeEnum::Pan, ConnectorTypeEnum::cCCS1}, + {ConnectorTypeEnum::Pan, ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::cType1}, + {ConnectorTypeEnum::Pan, ConnectorTypeEnum::cType1, ConnectorTypeEnum::cCCS1}, + {ConnectorTypeEnum::cType1, ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::Pan}, + {ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::Pan, ConnectorTypeEnum::cType1}, + {ConnectorTypeEnum::cCCS1, ConnectorTypeEnum::cType1, ConnectorTypeEnum::Pan}})); +} + +TEST_F(ReservationHandlerTest, specific_evse_scenario_01) { + // Test reservations for a specific evse. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // On EVSE1, there is no cCCS1 type connector. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS1)), + ReservationResult::Rejected); + // But there is a cCCS2 type connector, accept reservation. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // Another reservation on cCCS1 type connector will return Occupied, as there already is a reservation. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // But on another connector, the reservation can be made. + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // But only when there is not already a reservation for that specific connector. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, specific_evse_scenario_02) { + // Test reservations for a specific evse. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // No cCCS2 type on evse 1 (only on 0, but that one is not reserved here). + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Rejected); + // But it has a cType2 connector. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + // There already is a reservation for this EVSE, so 'Occupied' is returned. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // But on the other EVSE, the reservation can be made. + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // And now it is already reserved, so a second can not be made. + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_specific_evse_combination_scenario_01) { + // Test global reservation (not bound to specific EVSE) combined with reservation for a specific EVSE. + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + + // Global reservation for cType2, this can be EVSE 0 or 1. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + // Specific reservation for EVSE 1, the global reservation can still charge on EVSE 0. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // There already is a reservation for EVSE 1. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // Specific reservation for EVSE 2, the global reservation can still charge on EVSE 0. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // Specific reservation for EVSE 0, but if this would be accepted, the global reservation can not charge anymore, so + // this is denied. + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // EVSE 1 is already occupied with a reservation. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // Global reservation, can not be made because then the first reservation can not charge anymore. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // Same for a cCCS2 global reservation. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_specific_evse_combination_scenario_02) { + // Test global reservation (not bound to specific EVSE) combined with reservation for a specific EVSE. + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + + // Global reservation for cType2, this can charge on EVSE 0 or 1. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + // Specific reservation for EVSE 1, the global reservation still has an EVSE left to charge on. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // EVSE 1 is already reserved. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // Another global reservation. This can not be made, because the first global reservation would not have been able + // to charge in that case. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // But a global reservation for cCCS2 is possible. Because as EVSE 1 is reserved, there is only one option left for + // this reservation, which is EVSE 2, and the first reservation can then still charge on EVSE 0. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // But another global reservation is not possible anymore. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // As well as any other specific reservation. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, global_reservation_specific_evse_combination_scenario_03) { + // Test global reservation (not bound to specific EVSE) combined with reservation for a specific EVSE. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + // Make a reservation for EVSE 2. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + // EVSE 2 already has a reservation. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // Make a reservation for EVSE 1. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + // A global reservation is possible, because EVSE 0 is still not reserved and has cCCS2. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // But another global reservation is not possible, because there are not enough cCCS2 connectors. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + // And cType2 is also not possible, because it can arrive before the first global reservation and then put the car + // at EVSE 0, and then there will be no place for the car that did the first global reservation. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + // But a specific reservation for EVSE 3 is possible, because the first global reservation can then still charge + // at EVSE 0. + EXPECT_EQ(r.make_reservation(3, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); +} + +TEST_F(ReservationHandlerTest, check_charging_possible_global_specific_reservations_scenario_01) { + // Do some specific reservations and check if charging is possible when a car arrives. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + // Charging is possible on all EVSE's (except for the not existing one of course). + EXPECT_TRUE(r.is_charging_possible(0)); + EXPECT_TRUE(r.is_charging_possible(2)); + EXPECT_TRUE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(3)); + EXPECT_FALSE(r.is_charging_possible(4)); + // But after a reservation on EVSE 2, charging is not possible on that EVSE anymore. + EXPECT_EQ(r.make_reservation(2, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_TRUE(r.is_charging_possible(0)); + EXPECT_FALSE(r.is_charging_possible(2)); + EXPECT_TRUE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(3)); + // Now EVSE 1 is also occupied, charging will also not be possible there. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_TRUE(r.is_charging_possible(0)); + EXPECT_FALSE(r.is_charging_possible(2)); + EXPECT_FALSE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(3)); + // A global reservation have been made for a cCCS2 charger. The only still available is the one on EVSE 0. That one + // must be available for the reservation at all times. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + // So that makes charging on EVSE 0 not possible. + EXPECT_FALSE(r.is_charging_possible(0)); + // But the car can charge at EVSE 3 (as EVSE 0 is then still available for the global reservation). + EXPECT_TRUE(r.is_charging_possible(3)); + // And now all reservations are made, no new car can make a reservation or charge anymore. + EXPECT_EQ(r.make_reservation(3, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_FALSE(r.is_charging_possible(0)); + EXPECT_FALSE(r.is_charging_possible(1)); + EXPECT_FALSE(r.is_charging_possible(2)); + EXPECT_FALSE(r.is_charging_possible(3)); +} + +TEST_F(ReservationHandlerTest, check_charging_possible_global_specific_reservations_scenario_02) { + // Do some specific reservations and check if charging is possible when a car arrives. + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + + // Charging is possible on all EVSE's. + EXPECT_TRUE(r.is_charging_possible(0)); + EXPECT_TRUE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(2)); + // After a global reservation for cType2, charging is still possible on all EVSE's, is there are two cType2 + // connectors, so when one car charges, there is still a cType2 available. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_TRUE(r.is_charging_possible(0)); + EXPECT_TRUE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(2)); + // A reservation for EVSE 1 is made. Now the global reservation only has the possibility to charge on EVSE 0. + // So on that EVSE, charging is not possible anymore. And of course also not on EVSE 1 as that one is reserved. + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_FALSE(r.is_charging_possible(0)); + EXPECT_FALSE(r.is_charging_possible(1)); + EXPECT_TRUE(r.is_charging_possible(2)); + // Another global reservation makes charging impossible on all EVSE's. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_FALSE(r.is_charging_possible(0)); + EXPECT_FALSE(r.is_charging_possible(1)); + EXPECT_FALSE(r.is_charging_possible(2)); +} + +TEST_F(ReservationHandlerTest, is_evse_reserved) { + // Check if a specific EVSE is reserved. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_FALSE(r.is_evse_reserved(0)); + EXPECT_FALSE(r.is_evse_reserved(1)); + + // After a global reservation, no specific EVSE is still reserved. + r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)); + + EXPECT_FALSE(r.is_evse_reserved(0)); + EXPECT_FALSE(r.is_evse_reserved(1)); + + // But after a specific reservation, the EVSE of that reservation is reserved. + r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)); + + EXPECT_FALSE(r.is_evse_reserved(0)); + EXPECT_TRUE(r.is_evse_reserved(1)); +} + +TEST_F(ReservationHandlerTest, change_availability_scenario_01) { + // Change availability of an EVSE and check if reservations are cancelled. + std::optional evse_id; + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // Four global reservations are made. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + // Set an evse to not available, this will call the cancel reservation callback for the last reserved reservation + // id + EXPECT_CALL(reservation_callback_mock, Call(_, 3, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + EXPECT_FALSE(evse_id.has_value()); + + // Setting an evse to faulted will cancel the next reservation. + EXPECT_CALL(reservation_callback_mock, Call(_, 2, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + EXPECT_FALSE(evse_id.has_value()); + + // Set evse to available again. This will not call a cancelled callback. And setting one to unavailable will also + // not cause the cancelled callback to be called because there is still one evse available. + EXPECT_CALL(reservation_callback_mock, Call(_, 2, ReservationEndReason::Cancelled, true)).Times(0); + + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::ERROR_CLEARED); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + + this->evses[2]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[2]->connectors.at(1).get_state(), 2, 1); + + // If we set even one more evse to unavailable (or actually, to faulted), this will cancel the next (or actually + // previous) reservation. + EXPECT_CALL(reservation_callback_mock, Call(_, 1, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(1).get_state(), 0, 1); + EXPECT_FALSE(evse_id.has_value()); +} + +TEST_F(ReservationHandlerTest, change_availability_scenario_02) { + // Change availability of an EVSE and check if reservations are cancelled. This time, global and specific EVSE + // reservations mixed. + std::optional evse_id; + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(3, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + // Set an evse to not available, this will call the cancel reservation callback for the reservation of that evse id. + EXPECT_CALL(reservation_callback_mock, Call(_, 0, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + ASSERT_TRUE(evse_id.has_value()); + EXPECT_EQ(evse_id.value(), 1); + + // Setting an evse to faulted will cancel the next reservation (last made), this will be a 'global' reservation as + // there is no evse specific reservation made. + EXPECT_CALL(reservation_callback_mock, Call(_, 3, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + this->evses[2]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[2]->connectors.at(1).get_state(), 2, 1); + EXPECT_FALSE(evse_id.has_value()); + + // Set one more evse to unavailable, this will cancel the next reservation. + EXPECT_CALL(reservation_callback_mock, Call(_, 2, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(1).get_state(), 0, 1); + + // r.set_evse_state(ConnectorState::FAULTED, 0); + EXPECT_FALSE(evse_id.has_value()); + + // Set the last evse to unavailable will cancel the reservation of that specific evse. + EXPECT_CALL(reservation_callback_mock, Call(_, 1, ReservationEndReason::Cancelled, true)) + .WillOnce(SaveArg<0>(&evse_id)); + + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + ASSERT_TRUE(evse_id.has_value()); + EXPECT_EQ(evse_id.value(), 3); +} + +TEST_F(ReservationHandlerTest, reservation_evse_unavailable) { + // Set evse unavailable and check if a reservation can not be made in that case. Global reservations. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[1]->connectors.at(0).get_state(), 1, 0); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + // r.set_evse_state(ConnectorState::UNAVAILABLE, 1); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[0]->connectors.at(0).get_state(), 0, 0); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[0]->connectors.at(0).get_state(), 0, 1); + this->evses[2]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[2]->connectors.at(0).get_state(), 2, 0); + this->evses[2]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[2]->connectors.at(0).get_state(), 2, 1); + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Unavailable); +} + +TEST_F(ReservationHandlerTest, reservation_specific_evse_unavailable) { + // Set an EVSE to unavailable and check if that specific EVSE can not be reserved anymore. + + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Unavailable); +} + +TEST_F(ReservationHandlerTest, reservation_specific_evse_faulted) { + // Set an EVSE to faulted and check if that specific EVSE can not be reserved anymore. + + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // Evse state is faulted, should return faulted. + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(0).get_state(), 0, 0); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(1).get_state(), 0, 1); + + EXPECT_EQ(r.make_reservation(0, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Faulted); + + // Connectors are faulted, should return faulted. + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(0).get_state(), 1, 0); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Faulted); +} + +TEST_F(ReservationHandlerTest, reservation_evse_faulted) { + // Set EVSE's to faulted and check if no global reservations can made for that EVSE. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(0).get_state(), 1, 0); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + + // One EVSE is faulted and there are only two cCCS2 connectors left. So only two global reservations for cCCS2 can + // be made. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + + // Everything is faulted now, a reservation is not possible anymore. + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(0).get_state(), 0, 0); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[0]->connectors.at(1).get_state(), 0, 1); + this->evses[2]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[2]->connectors.at(0).get_state(), 2, 0); + this->evses[2]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[2]->connectors.at(1).get_state(), 2, 1); + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + + // All EVSE's are faulted, so 'Faulted' is returned. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Faulted); +} + +TEST_F(ReservationHandlerTest, reservation_evse_unavailable_and_faulted) { + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // Set evse to faulted. + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(0).get_state(), 1, 0); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Occupied); + + // Set all other evse's to unavailable, but not faulted. + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[0]->connectors.at(0).get_state(), 0, 0); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[0]->connectors.at(1).get_state(), 0, 1); + this->evses[2]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[2]->connectors.at(0).get_state(), 2, 0); + this->evses[2]->connectors.at(1).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[2]->connectors.at(1).get_state(), 2, 1); + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + r.on_connector_state_changed(this->evses[3]->connectors.at(0).get_state(), 3, 1); + + // At least one evse is faulted, so 'faulted' is returned. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Faulted); +} + +TEST_F(ReservationHandlerTest, reservation_connector_all_faulted) { + // Set all connectors to 'Faulted', no reservation can be made and the function will return 'Faulted'. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(3, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(3, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + this->evses[0]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + this->evses[3]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + this->evses[3]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Faulted); +} + +TEST_F(ReservationHandlerTest, reservation_connector_unavailable) { + // Set specific connectors to 'Unavailable' and try to make reservations. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS1, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType1, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + this->evses[0]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::DISABLE); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + + // There is a reservation already made, so this will return 'occupied'. + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS1)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType1)), + ReservationResult::Occupied); +} + +TEST_F(ReservationHandlerTest, reservation_in_the_past) { + // Try to create a reservation in the past, this should be rejected. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2); + reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() - std::chrono::hours(2)); + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Rejected); +} + +TEST_F(ReservationHandlerTest, reservation_timer) { + // Test the reservation timer: after the time has expired, the reservation should be cancelled. + std::optional evse_id; + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_CALL(reservation_callback_mock, Call(_, 0, ReservationEndReason::Expired, true)) + .WillOnce(SaveArg<0>(&evse_id)); + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2); + reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::seconds(1)); + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + sleep(1); + EXPECT_FALSE(evse_id.has_value()); + + EXPECT_CALL(reservation_callback_mock, Call(_, 0, ReservationEndReason::Expired, true)) + .WillOnce(SaveArg<0>(&evse_id)); + reservation.expiry_time = Everest::Date::to_rfc3339(date::utc_clock::now() + std::chrono::seconds(1)); + EXPECT_EQ(r.make_reservation(0, reservation), ReservationResult::Accepted); + sleep(1); + ASSERT_TRUE(evse_id.has_value()); + EXPECT_EQ(evse_id.value(), 0); +} + +TEST_F(ReservationHandlerTest, cancel_reservation) { + // Cancel reservation and check if a new reservation can be made after an old one is cancelled. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + + std::pair> reservation_cancelled_check_value; + + // There was no reservation with id 5. + reservation_cancelled_check_value = {false, std::nullopt}; + EXPECT_EQ(r.cancel_reservation(5, false, ReservationEndReason::Cancelled), reservation_cancelled_check_value); + + // There was a reservation with id 1, it had no evse id (global reservation). + reservation_cancelled_check_value = {true, std::nullopt}; + EXPECT_EQ(r.cancel_reservation(1, false, ReservationEndReason::Cancelled), reservation_cancelled_check_value); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + // There was a reservation with id 3, it was made for evse id 1. + reservation_cancelled_check_value = {true, 1}; + EXPECT_EQ(r.cancel_reservation(3, false, ReservationEndReason::Cancelled), reservation_cancelled_check_value); +} + +TEST_F(ReservationHandlerTest, overwrite_reservation) { + // If a reservation is made and another one is made with the same reservation id, it should be overwritten. + // The old reservation will then be cancelled and the new one is made. + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + + add_connector(5, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(5, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_CALL(reservation_callback_mock, Call(_, 0, ReservationEndReason::Cancelled, false)).Times(0); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + EXPECT_EQ(r.make_reservation(5, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(5, reservation), ReservationResult::Accepted); +} + +TEST_F(ReservationHandlerTest, matches_reserved_identifier) { + // Check if token or parent token matches with a reservation. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(2, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(2, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation.parent_id_token = "PARENT_TOKEN_0"; + Reservation reservation2 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation2.parent_id_token = "PARENT_TOKEN_2"; + Reservation reservation3 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation3.parent_id_token = "PARENT_TOKEN_3"; + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(1, reservation2), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(2, reservation3), ReservationResult::Accepted); + + // Id token is correct and evse id as well. + EXPECT_EQ(r.matches_reserved_identifier(reservation.id_token, std::nullopt, std::nullopt), 0); + // Id token is correct and evse id as well, parent token is not but that is ignored since the normal token is ok. + EXPECT_EQ(r.matches_reserved_identifier(reservation.id_token, std::nullopt, "WRONG_PARENT_TOKEN"), 0); + // Token is wrong. + EXPECT_EQ(r.matches_reserved_identifier("WRONG_TOKEN", std::nullopt, std::nullopt), std::nullopt); + // Evse id reservation does not have parent token, do not search in global reservation. + EXPECT_EQ(r.matches_reserved_identifier(reservation.id_token, 1, std::nullopt), std::nullopt); + // Evse id is wrong. + EXPECT_EQ(r.matches_reserved_identifier(reservation2.id_token, 2, std::nullopt), std::nullopt); + // Token is wrong but parent token is correct. + EXPECT_EQ(r.matches_reserved_identifier("WRONG_TOKEN", std::nullopt, "PARENT_TOKEN_0"), 0); + // Token is wrong and parent token as well. + EXPECT_EQ(r.matches_reserved_identifier("WRONG_TOKEN", std::nullopt, "WRONG_PARENT_TOKEN"), std::nullopt); + // Evse id is correct and token is correct. + EXPECT_EQ(r.matches_reserved_identifier(reservation2.id_token, 1, std::nullopt), 1); + // Evse id is correct but token is wrong. + EXPECT_EQ(r.matches_reserved_identifier("TOKEN_NOK", 1, std::nullopt), std::nullopt); + // Evse id is wrong and token is correct. + EXPECT_EQ(r.matches_reserved_identifier(reservation2.id_token, 2, std::nullopt), std::nullopt); + // Evse id is correct, token is wrong but parent token is correct. + EXPECT_EQ(r.matches_reserved_identifier("TOKEN_NOK", 1, "PARENT_TOKEN_2"), 1); + // Evse id is correct, token is wrong and parent token as well. + EXPECT_EQ(r.matches_reserved_identifier("TOKEN_NOK", 1, "PARENT_TOKEN_NOK"), std::nullopt); +} + +TEST_F(ReservationHandlerTest, has_reservation_parent_id) { + // Check if the reservation has a parent id token. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation.parent_id_token = "PARENT_TOKEN_0"; + Reservation reservation2 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation2.parent_id_token = "PARENT_TOKEN_2"; + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(1, reservation2), ReservationResult::Accepted); + + // Id token is correct and evse id as well. + EXPECT_TRUE(r.has_reservation_parent_id(std::nullopt)); + EXPECT_TRUE(r.has_reservation_parent_id(1)); + EXPECT_TRUE(r.has_reservation_parent_id(0)); + // Evse id does not exist. + EXPECT_FALSE(r.has_reservation_parent_id(2)); +} + +TEST_F(ReservationHandlerTest, has_reservation_parent_id_no_parent_token) { + // Check if the reservation has a parent id token. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + Reservation reservation2 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(1, reservation2), ReservationResult::Accepted); + + // No parent id tokens + EXPECT_FALSE(r.has_reservation_parent_id(std::nullopt)); + EXPECT_FALSE(r.has_reservation_parent_id(1)); + EXPECT_FALSE(r.has_reservation_parent_id(0)); + // Evse id does not exist. + EXPECT_FALSE(r.has_reservation_parent_id(2)); +} + +TEST_F(ReservationHandlerTest, has_reservation_parent_id_evse_reservation_parent_token) { + // Check if the reservation has a parent id token. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + Reservation reservation2 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation2.parent_id_token = "PARENT_TOKEN_2"; + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(1, reservation2), ReservationResult::Accepted); + + // Only evse id 1 reservation has parent id token. + EXPECT_FALSE(r.has_reservation_parent_id(std::nullopt)); + EXPECT_TRUE(r.has_reservation_parent_id(1)); + // So evse id 0 has not. + EXPECT_FALSE(r.has_reservation_parent_id(0)); + // Evse id does not exist. + EXPECT_FALSE(r.has_reservation_parent_id(2)); +} + +TEST_F(ReservationHandlerTest, has_reservation_parent_id_global_reservation_parent_token) { + // Check if the reservation has a parent id token. + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + Reservation reservation = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + reservation.parent_id_token = "PARENT_TOKEN_0"; + Reservation reservation2 = create_reservation(types::evse_manager::ConnectorTypeEnum::cType2); + EXPECT_EQ(r.make_reservation(std::nullopt, reservation), ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(1, reservation2), ReservationResult::Accepted); + + // Only global reservation has parent id token. Reservation on evse id 1 has none. + EXPECT_TRUE(r.has_reservation_parent_id(std::nullopt)); + EXPECT_FALSE(r.has_reservation_parent_id(1)); + // No reservation for evse id 0, but global reservation has parent id token. + EXPECT_TRUE(r.has_reservation_parent_id(0)); + // Evse id does not exist. + EXPECT_FALSE(r.has_reservation_parent_id(2)); +} + +TEST_F(ReservationHandlerTest, on_reservation_used) { + // A reservation is made and later used, so the reservation should be removed and the EVSE available again. + + // Register a callback, which should not be called. + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + + EXPECT_CALL(reservation_callback_mock, Call(_, _, _, true)).Times(3); + + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + + r.on_reservation_used(1); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Occupied); + + r.on_reservation_used(0); + r.on_reservation_used(2); + r.on_reservation_used(3); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); +} + +TEST_F(ReservationHandlerTest, store_load_reservations) { + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + + r.load_reservations(); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + EXPECT_EQ(r.evse_reservations.size(), 1); + EXPECT_EQ(r.global_reservations.size(), 1); + EXPECT_EQ(r.reservation_id_to_reservation_timeout_timer_map.size(), 2); + + r.evse_reservations.clear(); + r.global_reservations.clear(); + r.reservation_id_to_reservation_timeout_timer_map.clear(); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + EXPECT_TRUE(r.reservation_id_to_reservation_timeout_timer_map.empty()); + + r.load_reservations(); + + EXPECT_EQ(r.evse_reservations.size(), 1); + EXPECT_EQ(r.global_reservations.size(), 1); + EXPECT_EQ(r.reservation_id_to_reservation_timeout_timer_map.size(), 2); +} + +TEST_F(ReservationHandlerTest, store_load_reservations_connector_unavailable) { + add_connector(0, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(0, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + add_connector(1, 0, types::evse_manager::ConnectorTypeEnum::cCCS2, this->evses); + add_connector(1, 1, types::evse_manager::ConnectorTypeEnum::cType2, this->evses); + + // Register a callback, which should not be called. + MockFunction& evse_id, const int32_t reservation_id, + const ReservationEndReason reason, const bool send_reservation_update)> + reservation_callback_mock; + + r.register_reservation_cancelled_callback(reservation_callback_mock.AsStdFunction()); + + EXPECT_CALL(reservation_callback_mock, Call(_, _, _, true)).Times(1); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + + r.load_reservations(); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + EXPECT_EQ(r.evse_reservations.size(), 1); + EXPECT_EQ(r.global_reservations.size(), 1); + EXPECT_EQ(r.reservation_id_to_reservation_timeout_timer_map.size(), 2); + + r.evse_reservations.clear(); + r.global_reservations.clear(); + r.reservation_id_to_reservation_timeout_timer_map.clear(); + + EXPECT_TRUE(r.evse_reservations.empty()); + EXPECT_TRUE(r.global_reservations.empty()); + EXPECT_TRUE(r.reservation_id_to_reservation_timeout_timer_map.empty()); + + this->evses[1]->connectors.at(0).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(0).get_state(), 3, 0); + this->evses[1]->connectors.at(1).submit_event(ConnectorEvent::FAULTED); + r.on_connector_state_changed(this->evses[1]->connectors.at(1).get_state(), 1, 1); + + r.load_reservations(); + + EXPECT_EQ(r.evse_reservations.size(), 0); + EXPECT_EQ(r.global_reservations.size(), 1); + EXPECT_EQ(r.reservation_id_to_reservation_timeout_timer_map.size(), 1); +} + +} // namespace module diff --git a/modules/Auth/tests/stubs/generated/interfaces/kvs/Interface.hpp b/modules/Auth/tests/stubs/generated/interfaces/kvs/Interface.hpp new file mode 100644 index 000000000..52d69a4aa --- /dev/null +++ b/modules/Auth/tests/stubs/generated/interfaces/kvs/Interface.hpp @@ -0,0 +1,35 @@ +#ifndef KVS_INTERFACE_HPP +#define KVS_INTERFACE_HPP + +#include +#include +#include + +#include +// #include + +using nlohmann::json; + +using Array = nlohmann::json::array_t; +using Object = nlohmann::json::object_t; + +class kvsIntf { +private: + std::variant value; + +public: + kvsIntf() { + } + void call_store(std::string key, + std::variant value) { + std::cout << "Store called!" << std::endl; + this->value = value; + } + std::variant call_load(std::string key) { + std::cout << "Load called!" << std::endl; + + return this->value; + } +}; + +#endif // KVS_INTERFACE_HPP diff --git a/modules/DummyTokenProvider/main/auth_token_providerImpl.hpp b/modules/DummyTokenProvider/main/auth_token_providerImpl.hpp index c966158d0..dd98615b7 100644 --- a/modules/DummyTokenProvider/main/auth_token_providerImpl.hpp +++ b/modules/DummyTokenProvider/main/auth_token_providerImpl.hpp @@ -31,7 +31,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "main"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "main"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/DummyTokenProviderManual/main/auth_token_providerImpl.hpp b/modules/DummyTokenProviderManual/main/auth_token_providerImpl.hpp index db6f50c72..667da35e8 100644 --- a/modules/DummyTokenProviderManual/main/auth_token_providerImpl.hpp +++ b/modules/DummyTokenProviderManual/main/auth_token_providerImpl.hpp @@ -30,7 +30,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "main"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "main"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 1a3002e3b..5b6aa4e76 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -96,7 +96,7 @@ void EvseManager::init() { } reserved = false; - reservation_id = 0; + reservation_id = -1; hlc_waiting_for_auth_eim = false; hlc_waiting_for_auth_pnc = false; @@ -1238,34 +1238,53 @@ bool EvseManager::update_max_current_limit(types::energy::ExternalLimits& limits return true; } -bool EvseManager::reserve(int32_t id) { +bool EvseManager::reserve(int32_t id, const bool signal_reservation_event) { + EVLOG_debug << "Reserve called for reservation id " << id + << ", signal reservation event: " << signal_reservation_event; // is the evse Unavailable? if (charger->get_current_state() == Charger::EvseState::Disabled) { + EVLOG_info << "Rejecting reservation because charger is disabled."; return false; } // is the evse faulted? if (charger->stop_charging_on_fatal_error()) { + EVLOG_info << "Rejecting reservation because of a fatal error."; return false; } // is the connector currently ready to accept a new car? if (charger->get_current_state() not_eq Charger::EvseState::Idle) { + EVLOG_info << "Rejecting reservation because evse is not idle"; return false; } Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_reserve); - if (not reserved) { + const bool overwrite_reservation = (this->reservation_id == id); + + if (reserved) { + EVLOG_info << "Rejecting reservation because evse is already reserved"; + } + + // Check if this evse is not already reserved, or overwrite reservation if it is for the same reservation id. + if (not reserved || overwrite_reservation) { + EVLOG_debug << "Make the reservation with id " << id; reserved = true; reservation_id = id; - // publish event to other modules - types::evse_manager::SessionEvent se; - se.event = types::evse_manager::SessionEventEnum::ReservationStart; + // When overwriting the reservation, don't signal. + if (not overwrite_reservation && signal_reservation_event) { + // publish event to other modules + types::evse_manager::SessionEvent se; + se.event = types::evse_manager::SessionEventEnum::ReservationStart; + + // Normally we should signal for each connector when an evse is reserved, but since in this implementation + // each evse only has one connector, this is sufficient for now. + signalReservationEvent(se); + } - signalReservationEvent(se); return true; } @@ -1276,8 +1295,9 @@ void EvseManager::cancel_reservation(bool signal_event) { Everest::scoped_lock_timeout lock(reservation_mutex, Everest::MutexDescription::EVSE_cancel_reservation); if (reserved) { + EVLOG_debug << "Reservation cancelled"; reserved = false; - reservation_id = 0; + reservation_id = -1; // publish event to other modules if (signal_event) { diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index dce749cf5..7fc47dacc 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -52,6 +52,7 @@ namespace module { struct Conf { int connector_id; + std::string connector_type; std::string evse_id; std::string evse_id_din; bool payment_enable_eim; @@ -134,7 +135,8 @@ class EvseManager : public Everest::ModuleBase { r_imd(std::move(r_imd)), r_powersupply_DC(std::move(r_powersupply_DC)), r_store(std::move(r_store)), - config(config){}; + config(config) { + } Everest::MqttProvider& mqtt; Everest::TelemetryProvider& telemetry; @@ -172,7 +174,15 @@ class EvseManager : public Everest::ModuleBase { void cancel_reservation(bool signal_event); bool is_reserved(); - bool reserve(int32_t id); + + /// + /// \brief Reserve this evse. + /// \param id The reservation id. + /// \param signal_reservation_event True when other modules must be signalled about a new reservation (session + /// event). + /// \return True on success. + /// + bool reserve(int32_t id, const bool signal_reservation_event = true); int32_t get_reservation_id(); bool get_hlc_enabled(); @@ -232,8 +242,8 @@ class EvseManager : public Everest::ModuleBase { r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits); // HLC layer will also get new maximum current/voltage/watt limits etc, but those will need to run through - // energy management first. Those limits will be applied in energy_grid implementation when requesting energy, - // so it is enough to set the powersupply_capabilities here. + // energy management first. Those limits will be applied in energy_grid implementation when requesting + // energy, so it is enough to set the powersupply_capabilities here. // FIXME: this is not implemented yet: enforce_limits uses the enforced limits to tell HLC, but capabilities // limits are not yet included in request. diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index 103266bbb..4b8f424d8 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -289,6 +289,11 @@ void evse_managerImpl::ready() { session_finished.meter_value = mod->get_latest_powermeter_data_billing(); se.session_finished = session_finished; session_log.evse(false, fmt::format("Session Finished")); + // Cancel reservation, reservation might be stored when swiping rfid, but timed out, so we should not + // set the reservation id here. + if (mod->is_reserved()) { + mod->cancel_reservation(false); + } session_log.stopSession(); mod->telemetry.publish("session", "events", {{"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, @@ -357,11 +362,20 @@ types::evse_manager::Evse evse_managerImpl::handle_get_evse() { types::evse_manager::Evse evse; evse.id = this->mod->config.connector_id; - // EvseManager currently only supports a single connector with id: 1; std::vector connectors; types::evse_manager::Connector connector; + // EvseManager currently only supports a single connector with id: 1; connector.id = 1; + if (!this->mod->config.connector_type.empty()) { + try { + connector.type = types::evse_manager::string_to_connector_type_enum(this->mod->config.connector_type); + } catch (const std::out_of_range& e) { + EVLOG_warning << "Evse with id " << evse.id << ": connector type invalid: " << e.what(); + } + } + connectors.push_back(connector); + evse.connectors = connectors; return evse; } @@ -384,6 +398,16 @@ void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedI this->mod->charger->authorize(true, provided_token); mod->charger_was_authorized(); + if (validation_result.reservation_id.has_value()) { + EVLOG_debug << "Reserve evse manager reservation id for id " << validation_result.reservation_id.value(); + // The validation result returns a reservation id. If this was a reservation for a specific evse, the + // evse manager probably already stored the reservation id (and this call is not really necessary). But if + // the reservation was not for a specific evse, the evse manager still has to send the reservation id in the + // transaction event request. So that is why we call 'reserve' here, so the evse manager knows the + // reservation id that belongs to this specific session and can send it accordingly. + // As this is not a new reservation but an existing one, we don't signal a reservation event for this. + mod->reserve(validation_result.reservation_id.value(), false); + } } if (pnc) { @@ -398,7 +422,7 @@ void evse_managerImpl::handle_withdraw_authorization() { }; bool evse_managerImpl::handle_reserve(int& reservation_id) { - return mod->reserve(reservation_id); + return mod->reserve(reservation_id, true); }; void evse_managerImpl::handle_cancel_reservation() { diff --git a/modules/EvseManager/evse/evse_managerImpl.hpp b/modules/EvseManager/evse/evse_managerImpl.hpp index 58ac2e3fb..e316dd5d2 100644 --- a/modules/EvseManager/evse/evse_managerImpl.hpp +++ b/modules/EvseManager/evse/evse_managerImpl.hpp @@ -25,7 +25,8 @@ class evse_managerImpl : public evse_managerImplBase { public: evse_managerImpl() = delete; evse_managerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - evse_managerImplBase(ev, "evse"), mod(mod), config(config){}; + evse_managerImplBase(ev, "evse"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index 70c921e8a..a40810d25 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -6,6 +6,10 @@ config: connector_id: description: Connector id of this evse manager type: integer + connector_type: + description: The connector type of this evse manager (/evse_manager#/ConnectorTypeEnum) + type: string + default: "Unknown" evse_id: description: EVSE ID type: string diff --git a/modules/EvseManager/token_provider/auth_token_providerImpl.hpp b/modules/EvseManager/token_provider/auth_token_providerImpl.hpp index 6bcb6db58..171fe2aec 100644 --- a/modules/EvseManager/token_provider/auth_token_providerImpl.hpp +++ b/modules/EvseManager/token_provider/auth_token_providerImpl.hpp @@ -25,7 +25,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { public: auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "token_provider"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "token_provider"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/EvseV2G/iso_server.cpp b/modules/EvseV2G/iso_server.cpp index 58c290ed4..21babf8b4 100644 --- a/modules/EvseV2G/iso_server.cpp +++ b/modules/EvseV2G/iso_server.cpp @@ -2031,7 +2031,7 @@ enum v2g_event iso_handle_request(v2g_connection* conn) { conn->ctx->current_v2g_msg = V2G_SERVICE_DISCOVERY_MSG; exi_out->V2G_Message.Body.ServiceDiscoveryRes_isUsed = 1u; init_iso2_ServiceDiscoveryResType(&exi_out->V2G_Message.Body.ServiceDiscoveryRes); - next_v2g_event = handle_iso_service_discovery(conn); // [V2G2-542] + next_v2g_event = handle_iso_service_discovery(conn); // [V2G2-544] } else if (exi_in->V2G_Message.Body.ServiceDetailReq_isUsed) { dlog(DLOG_LEVEL_TRACE, "Handling ServiceDetailReq"); conn->ctx->current_v2g_msg = V2G_SERVICE_DETAIL_MSG; diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index 889758469..595044f89 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -551,10 +551,18 @@ void OCPP::ready() { reservation.id_token = idTag.get(); reservation.reservation_id = reservation_id; reservation.expiry_time = expiryDate.to_rfc3339(); + if (parent_id) { reservation.parent_id_token.emplace(parent_id.value().get()); } - auto response = this->r_reservation->call_reserve_now(connector, reservation); + + if (connector == 0) { + reservation.evse_id = std::nullopt; + } else { + reservation.evse_id = connector; + } + + auto response = this->r_reservation->call_reserve_now(reservation); return conversions::to_ocpp_reservation_status(response); }); @@ -715,6 +723,18 @@ void OCPP::ready() { [this](const std::string& data) { this->charge_point->disconnect_websocket(); }); } + this->charge_point->register_is_token_reserved_for_connector_callback( + [this](const int32_t connector, const std::string& id_token) -> ocpp::ReservationCheckStatus { + types::reservation::ReservationCheck reservation_check_request; + reservation_check_request.evse_id = connector; + reservation_check_request.id_token = id_token; + + types::reservation::ReservationCheckStatus status = + this->r_reservation->call_exists_reservation(reservation_check_request); + + return ocpp_conversions::to_ocpp_reservation_check_status(status); + }); + const auto composite_schedule_unit = get_unit_or_default(this->config.RequestCompositeScheduleUnit); // publish charging schedules at least once on startup diff --git a/modules/OCPP/OCPP.hpp b/modules/OCPP/OCPP.hpp index cc47f165f..05df5f132 100644 --- a/modules/OCPP/OCPP.hpp +++ b/modules/OCPP/OCPP.hpp @@ -97,7 +97,8 @@ class OCPP : public Everest::ModuleBase { r_security(std::move(r_security)), r_data_transfer(std::move(r_data_transfer)), r_display_message(std::move(r_display_message)), - config(config){}; + config(config) { + } Everest::MqttProvider& mqtt; const std::unique_ptr p_main; diff --git a/modules/OCPP/auth_provider/auth_token_providerImpl.hpp b/modules/OCPP/auth_provider/auth_token_providerImpl.hpp index a932ced71..a2d09c257 100644 --- a/modules/OCPP/auth_provider/auth_token_providerImpl.hpp +++ b/modules/OCPP/auth_provider/auth_token_providerImpl.hpp @@ -25,7 +25,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { public: auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "auth_provider"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "auth_provider"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/OCPP/auth_validator/auth_token_validatorImpl.hpp b/modules/OCPP/auth_validator/auth_token_validatorImpl.hpp index 076b8763b..7096f4cda 100644 --- a/modules/OCPP/auth_validator/auth_token_validatorImpl.hpp +++ b/modules/OCPP/auth_validator/auth_token_validatorImpl.hpp @@ -25,7 +25,8 @@ class auth_token_validatorImpl : public auth_token_validatorImplBase { public: auth_token_validatorImpl() = delete; auth_token_validatorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_validatorImplBase(ev, "auth_validator"), mod(mod), config(config){}; + auth_token_validatorImplBase(ev, "auth_validator"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/OCPP/ocpp_generic/ocppImpl.hpp b/modules/OCPP/ocpp_generic/ocppImpl.hpp index 6c09bb1f7..2b25a6bd3 100644 --- a/modules/OCPP/ocpp_generic/ocppImpl.hpp +++ b/modules/OCPP/ocpp_generic/ocppImpl.hpp @@ -25,7 +25,8 @@ class ocppImpl : public ocppImplBase { public: ocppImpl() = delete; ocppImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - ocppImplBase(ev, "ocpp_generic"), mod(mod), config(config){}; + ocppImplBase(ev, "ocpp_generic"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index cd93ce089..3e7b03427 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -299,26 +299,6 @@ void OCPP201::init() { } }); } - - const auto error_handler = [this](const Everest::error::Error& error) { - if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) { - // handled by specific evse_manager error handler - return; - } - const auto event_data = get_event_data(error, false, this->event_id_counter++); - this->charge_point->on_event({event_data}); - }; - - const auto error_cleared_handler = [this](const Everest::error::Error& error) { - if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) { - // handled by specific evse_manager error handler - return; - } - const auto event_data = get_event_data(error, true, this->event_id_counter++); - this->charge_point->on_event({event_data}); - }; - - subscribe_global_all_errors(error_handler, error_cleared_handler); } void OCPP201::ready() { @@ -469,11 +449,23 @@ void OCPP201::ready() { return conversions::to_ocpp_get_log_response(response); }; - callbacks.is_reservation_for_token_callback = [](const int32_t evse_id, const ocpp::CiString<36> idToken, - const std::optional> groupIdToken) { - // FIXME: This is just a stub, replace with functionality - EVLOG_warning << "is_reservation_for_token_callback is still a stub"; - return false; + callbacks.is_reservation_for_token_callback = [this](const int32_t evse_id, const ocpp::CiString<36> idToken, + const std::optional> groupIdToken) { + if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) { + return ocpp::ReservationCheckStatus::NotReserved; + } + + types::reservation::ReservationCheck reservation_check_request; + reservation_check_request.evse_id = evse_id; + reservation_check_request.id_token = idToken.get(); + if (groupIdToken.has_value()) { + reservation_check_request.group_id_token = groupIdToken.value().get(); + } + + const types::reservation::ReservationCheckStatus reservation_status = + this->r_reservation.at(0)->call_exists_reservation(reservation_check_request); + + return ocpp_conversions::to_ocpp_reservation_check_status(reservation_status); }; callbacks.update_firmware_request_callback = [this](const ocpp::v201::UpdateFirmwareRequest& request) { @@ -533,7 +525,6 @@ void OCPP201::ready() { std::promise promise; std::future future = promise.get_future(); ocpp::v201::ConfigNetworkResult result; - result.network_profile_slot = configuration_slot; result.success = true; promise.set_value(result); return future; @@ -689,6 +680,40 @@ void OCPP201::ready() { this->r_system->call_set_system_time(current_time.to_rfc3339()); }; + callbacks.reserve_now_callback = + [this](const ocpp::v201::ReserveNowRequest& request) -> ocpp::v201::ReserveNowStatusEnum { + ocpp::v201::ReserveNowResponse response; + if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) { + EVLOG_info << "Reservation rejected because the interface r_reservation is a nullptr"; + return ocpp::v201::ReserveNowStatusEnum::Rejected; + } + + types::reservation::Reservation reservation; + reservation.reservation_id = request.id; + reservation.expiry_time = request.expiryDateTime.to_rfc3339(); + reservation.id_token = request.idToken.idToken; + reservation.evse_id = request.evseId; + if (request.groupIdToken.has_value()) { + reservation.parent_id_token = request.groupIdToken.value().idToken; + } + if (request.connectorType.has_value()) { + reservation.connector_type = conversions::to_everest_connector_type_enum(request.connectorType.value()); + } + + types::reservation::ReservationResult result = this->r_reservation.at(0)->call_reserve_now(reservation); + return conversions::to_ocpp_reservation_status(result); + }; + + callbacks.cancel_reservation_callback = [this](const int32_t reservation_id) -> bool { + EVLOG_debug << "Received cancel reservation request for reservation id " << reservation_id; + ocpp::v201::CancelReservationResponse response; + if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) { + return false; + } + + return this->r_reservation.at(0)->call_cancel_reservation(reservation_id); + }; + const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRATIONS; std::map evse_connector_structure = this->get_connector_structure(); @@ -700,6 +725,26 @@ void OCPP201::ready() { this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath, std::make_shared(*this->r_security), callbacks); + const auto error_handler = [this](const Everest::error::Error& error) { + if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) { + // handled by specific evse_manager error handler + return; + } + const auto event_data = get_event_data(error, false, this->event_id_counter++); + this->charge_point->on_event({event_data}); + }; + + const auto error_cleared_handler = [this](const Everest::error::Error& error) { + if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) { + // handled by specific evse_manager error handler + return; + } + const auto event_data = get_event_data(error, true, this->event_id_counter++); + this->charge_point->on_event({event_data}); + }; + + subscribe_global_all_errors(error_handler, error_cleared_handler); + // publish charging schedules at least once on startup charging_schedules_callback(); @@ -832,6 +877,26 @@ void OCPP201::ready() { status.request_id); }); + if (!this->r_reservation.empty() && this->r_reservation.at(0) != nullptr) { + r_reservation.at(0)->subscribe_reservation_update( + [this](const types::reservation::ReservationUpdateStatus status) { + if (status.reservation_status == types::reservation::Reservation_status::Expired || + status.reservation_status == types::reservation::Reservation_status::Removed) { + EVLOG_debug << "Received reservation status update for reservation " << status.reservation_id + << ": " + << (status.reservation_status == types::reservation::Reservation_status::Expired + ? "Expired" + : "Removed"); + try { + this->charge_point->on_reservation_status( + status.reservation_id, + conversions::to_ocpp_reservation_update_status_enum(status.reservation_status)); + } catch (const std::out_of_range& e) { + } + } + }); + } + std::unique_lock lk(this->evse_ready_mutex); while (!this->all_evse_ready()) { this->evse_ready_cv.wait(lk); @@ -896,6 +961,14 @@ void OCPP201::process_session_event(const int32_t evse_id, const types::evse_man this->process_deauthorized(evse_id, connector_id, session_event); break; } + case types::evse_manager::SessionEventEnum::ReservationStart: { + this->process_reserved(evse_id, connector_id); + break; + } + case types::evse_manager::SessionEventEnum::ReservationEnd: { + this->process_reservation_end(evse_id, connector_id); + break; + } // explicitly ignore the following session events for now // TODO(kai): implement case types::evse_manager::SessionEventEnum::AuthRequired: @@ -903,8 +976,6 @@ void OCPP201::process_session_event(const int32_t evse_id, const types::evse_man case types::evse_manager::SessionEventEnum::WaitingForEnergy: case types::evse_manager::SessionEventEnum::StoppingCharging: case types::evse_manager::SessionEventEnum::ChargingFinished: - case types::evse_manager::SessionEventEnum::ReservationStart: - case types::evse_manager::SessionEventEnum::ReservationEnd: case types::evse_manager::SessionEventEnum::ReplugStarted: case types::evse_manager::SessionEventEnum::ReplugFinished: case types::evse_manager::SessionEventEnum::PluginTimeout: @@ -1207,6 +1278,14 @@ void OCPP201::process_deauthorized(const int32_t evse_id, const int32_t connecto this->process_tx_event_effect(evse_id, tx_event_effect, session_event); } +void OCPP201::process_reserved(const int32_t evse_id, const int32_t connector_id) { + this->charge_point->on_reserved(evse_id, connector_id); +} + +void OCPP201::process_reservation_end(const int32_t evse_id, const int32_t connector_id) { + this->charge_point->on_reservation_cleared(evse_id, connector_id); +} + void OCPP201::publish_charging_schedules(const std::vector& composite_schedules) { const auto everest_schedules = conversions::to_everest_charging_schedules(composite_schedules); this->p_ocpp_generic->publish_charging_schedules(everest_schedules); diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index ddff7352f..42ef16113 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -24,6 +24,7 @@ #include #include #include +#include #include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 @@ -61,7 +62,8 @@ class OCPP201 : public Everest::ModuleBase { std::unique_ptr r_security, std::vector> r_data_transfer, std::unique_ptr r_auth, std::vector> r_evse_energy_sink, - std::vector> r_display_message, Conf& config) : + std::vector> r_display_message, + std::vector> r_reservation, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), p_auth_validator(std::move(p_auth_validator)), @@ -76,6 +78,7 @@ class OCPP201 : public Everest::ModuleBase { r_auth(std::move(r_auth)), r_evse_energy_sink(std::move(r_evse_energy_sink)), r_display_message(std::move(r_display_message)), + r_reservation(std::move(r_reservation)), config(config) { } @@ -92,6 +95,7 @@ class OCPP201 : public Everest::ModuleBase { const std::unique_ptr r_auth; const std::vector> r_evse_energy_sink; const std::vector> r_display_message; + const std::vector> r_reservation; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -153,13 +157,14 @@ class OCPP201 : public Everest::ModuleBase { const types::evse_manager::SessionEvent& session_event); void process_deauthorized(const int32_t evse_id, const int32_t connector_id, const types::evse_manager::SessionEvent& session_event); + void process_reserved(const int32_t evse_id, const int32_t connector_id); + void process_reservation_end(const int32_t evse_id, const int32_t connector_id); /// \brief This function publishes the given \p composite_schedules via the ocpp interface void publish_charging_schedules(const std::vector& composite_schedules); /// \brief This function applies given \p composite_schedules for each connected evse_energy_sink void set_external_limits(const std::vector& composite_schedules); - // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/OCPP201/auth_provider/auth_token_providerImpl.hpp b/modules/OCPP201/auth_provider/auth_token_providerImpl.hpp index 05bdd6457..1fd60cd42 100644 --- a/modules/OCPP201/auth_provider/auth_token_providerImpl.hpp +++ b/modules/OCPP201/auth_provider/auth_token_providerImpl.hpp @@ -25,7 +25,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { public: auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "auth_provider"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "auth_provider"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/OCPP201/auth_validator/auth_token_validatorImpl.hpp b/modules/OCPP201/auth_validator/auth_token_validatorImpl.hpp index 7e3231409..33683a9df 100644 --- a/modules/OCPP201/auth_validator/auth_token_validatorImpl.hpp +++ b/modules/OCPP201/auth_validator/auth_token_validatorImpl.hpp @@ -25,7 +25,8 @@ class auth_token_validatorImpl : public auth_token_validatorImplBase { public: auth_token_validatorImpl() = delete; auth_token_validatorImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_validatorImplBase(ev, "auth_validator"), mod(mod), config(config){}; + auth_token_validatorImplBase(ev, "auth_validator"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/OCPP201/conversions.cpp b/modules/OCPP201/conversions.cpp index d3d78d8d6..ecfaf1ae8 100644 --- a/modules/OCPP201/conversions.cpp +++ b/modules/OCPP201/conversions.cpp @@ -827,6 +827,41 @@ to_ocpp_get_15118_certificate_request(const types::iso15118_charger::RequestExiS return _request; } +ocpp::v201::ReserveNowStatusEnum to_ocpp_reservation_status(const types::reservation::ReservationResult result) { + switch (result) { + case types::reservation::ReservationResult::Accepted: + return ocpp::v201::ReserveNowStatusEnum::Accepted; + case types::reservation::ReservationResult::Faulted: + return ocpp::v201::ReserveNowStatusEnum::Faulted; + case types::reservation::ReservationResult::Occupied: + return ocpp::v201::ReserveNowStatusEnum::Occupied; + case types::reservation::ReservationResult::Rejected: + return ocpp::v201::ReserveNowStatusEnum::Rejected; + case types::reservation::ReservationResult::Unavailable: + return ocpp::v201::ReserveNowStatusEnum::Unavailable; + } + + throw std::out_of_range("Could not convert ReservationResult"); +} + +ocpp::v201::ReservationUpdateStatusEnum +to_ocpp_reservation_update_status_enum(const types::reservation::Reservation_status status) { + switch (status) { + case types::reservation::Reservation_status::Expired: + return ocpp::v201::ReservationUpdateStatusEnum::Expired; + case types::reservation::Reservation_status::Removed: + return ocpp::v201::ReservationUpdateStatusEnum::Removed; + + case types::reservation::Reservation_status::Cancelled: + case types::reservation::Reservation_status::Placed: + case types::reservation::Reservation_status::Used: + // OCPP should not convert a status enum that is not an OCPP type. + throw std::out_of_range("Could not convert ReservationUpdateStatus: OCPP does not know this type"); + } + + throw std::out_of_range("Could not convert ReservationUpdateStatus"); +} + types::system::UploadLogsRequest to_everest_upload_logs_request(const ocpp::v201::GetLogRequest& request) { types::system::UploadLogsRequest _request; _request.location = request.log.remoteLocation.get(); @@ -1396,5 +1431,56 @@ to_ocpp_clear_display_message_response(const types::display_message::ClearDispla return result_response; } +types::evse_manager::ConnectorTypeEnum to_everest_connector_type_enum(const ocpp::v201::ConnectorEnum& connector_type) { + switch (connector_type) { + case ocpp::v201::ConnectorEnum::cCCS1: + return types::evse_manager::ConnectorTypeEnum::cCCS1; + case ocpp::v201::ConnectorEnum::cCCS2: + return types::evse_manager::ConnectorTypeEnum::cCCS2; + case ocpp::v201::ConnectorEnum::cG105: + return types::evse_manager::ConnectorTypeEnum::cG105; + case ocpp::v201::ConnectorEnum::cTesla: + return types::evse_manager::ConnectorTypeEnum::cTesla; + case ocpp::v201::ConnectorEnum::cType1: + return types::evse_manager::ConnectorTypeEnum::cType1; + case ocpp::v201::ConnectorEnum::cType2: + return types::evse_manager::ConnectorTypeEnum::cType2; + case ocpp::v201::ConnectorEnum::s309_1P_16A: + return types::evse_manager::ConnectorTypeEnum::s309_1P_16A; + case ocpp::v201::ConnectorEnum::s309_1P_32A: + return types::evse_manager::ConnectorTypeEnum::s309_1P_32A; + case ocpp::v201::ConnectorEnum::s309_3P_16A: + return types::evse_manager::ConnectorTypeEnum::s309_3P_16A; + case ocpp::v201::ConnectorEnum::s309_3P_32A: + return types::evse_manager::ConnectorTypeEnum::s309_3P_32A; + case ocpp::v201::ConnectorEnum::sBS1361: + return types::evse_manager::ConnectorTypeEnum::sBS1361; + case ocpp::v201::ConnectorEnum::sCEE_7_7: + return types::evse_manager::ConnectorTypeEnum::sCEE_7_7; + case ocpp::v201::ConnectorEnum::sType2: + return types::evse_manager::ConnectorTypeEnum::sType2; + case ocpp::v201::ConnectorEnum::sType3: + return types::evse_manager::ConnectorTypeEnum::sType3; + case ocpp::v201::ConnectorEnum::Other1PhMax16A: + return types::evse_manager::ConnectorTypeEnum::Other1PhMax16A; + case ocpp::v201::ConnectorEnum::Other1PhOver16A: + return types::evse_manager::ConnectorTypeEnum::Other1PhOver16A; + case ocpp::v201::ConnectorEnum::Other3Ph: + return types::evse_manager::ConnectorTypeEnum::Other3Ph; + case ocpp::v201::ConnectorEnum::Pan: + return types::evse_manager::ConnectorTypeEnum::Pan; + case ocpp::v201::ConnectorEnum::wInductive: + return types::evse_manager::ConnectorTypeEnum::wInductive; + case ocpp::v201::ConnectorEnum::wResonant: + return types::evse_manager::ConnectorTypeEnum::wResonant; + case ocpp::v201::ConnectorEnum::Undetermined: + return types::evse_manager::ConnectorTypeEnum::Undetermined; + case ocpp::v201::ConnectorEnum::Unknown: + return types::evse_manager::ConnectorTypeEnum::Unknown; + } + + throw std::out_of_range("Could not convert ConnectorEnum"); +} + } // namespace conversions } // namespace module diff --git a/modules/OCPP201/conversions.hpp b/modules/OCPP201/conversions.hpp index 51448a825..053dbf021 100644 --- a/modules/OCPP201/conversions.hpp +++ b/modules/OCPP201/conversions.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -122,6 +123,15 @@ ocpp::v201::AttributeEnum to_ocpp_attribute_enum(const types::ocpp::AttributeEnu ocpp::v201::Get15118EVCertificateRequest to_ocpp_get_15118_certificate_request(const types::iso15118_charger::RequestExiStreamSchema& request); +/// \brief Converts a given types::reservation::ReservationResult to ocpp::v201::ReserveNowStatusEnum +ocpp::v201::ReserveNowStatusEnum to_ocpp_reservation_status(const types::reservation::ReservationResult result); + +/// \brief Converts a given types::reservation::Reservation_status to ocpp::v201::ReservationUpdateStatusEnum +/// \warning This function can throw when there is no existing ocpp::v201::ReservationUpdateStatusEnum that is equal to +/// types::reservation::Reservation_status. +ocpp::v201::ReservationUpdateStatusEnum +to_ocpp_reservation_update_status_enum(const types::reservation::Reservation_status status); + /// \brief Converts a given ocpp::v201::ReasonEnum \p stop_reason to a types::evse_manager::StopTransactionReason. types::evse_manager::StopTransactionReason to_everest_stop_transaction_reason(const ocpp::v201::ReasonEnum& stop_reason); @@ -264,6 +274,9 @@ to_ocpp_clear_message_response_enum(const types::display_message::ClearMessageRe ocpp::v201::ClearDisplayMessageResponse to_ocpp_clear_display_message_response(const types::display_message::ClearDisplayMessageResponse& response); +/// \brief Convert a given ocpp::v201::ConnectorEnum connector type to a types::evse_manager::ConnectorTypeEnum +types::evse_manager::ConnectorTypeEnum to_everest_connector_type_enum(const ocpp::v201::ConnectorEnum& connector_type); + } // namespace conversions } // namespace module diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index 6023fc249..51df9abb6 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -98,6 +98,10 @@ requires: interface: display_message min_connections: 0 max_connections: 1 + reservation: + interface: reservation + min_connections: 0 + max_connections: 1 enable_external_mqtt: true enable_global_errors: true metadata: diff --git a/modules/OCPP201/ocpp_generic/ocppImpl.hpp b/modules/OCPP201/ocpp_generic/ocppImpl.hpp index 1d376967d..4fc270d8b 100644 --- a/modules/OCPP201/ocpp_generic/ocppImpl.hpp +++ b/modules/OCPP201/ocpp_generic/ocppImpl.hpp @@ -25,7 +25,8 @@ class ocppImpl : public ocppImplBase { public: ocppImpl() = delete; ocppImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - ocppImplBase(ev, "ocpp_generic"), mod(mod), config(config){}; + ocppImplBase(ev, "ocpp_generic"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/PN532TokenProvider/main/auth_token_providerImpl.hpp b/modules/PN532TokenProvider/main/auth_token_providerImpl.hpp index e4bd55c40..00266a7e5 100644 --- a/modules/PN532TokenProvider/main/auth_token_providerImpl.hpp +++ b/modules/PN532TokenProvider/main/auth_token_providerImpl.hpp @@ -33,7 +33,8 @@ class auth_token_providerImpl : public auth_token_providerImplBase { auth_token_providerImpl() = delete; auth_token_providerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - auth_token_providerImplBase(ev, "main"), mod(mod), config(config){}; + auth_token_providerImplBase(ev, "main"), mod(mod), config(config) { + } // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here diff --git a/modules/System/signed_firmware_downloader.sh b/modules/System/signed_firmware_downloader.sh index 903d40f6c..3ef3db44f 100755 --- a/modules/System/signed_firmware_downloader.sh +++ b/modules/System/signed_firmware_downloader.sh @@ -2,7 +2,7 @@ . "${1}" -mkdir /tmp/signature_validation +SIGNATURE_VALIDATION_DIR=$(mktemp -d /tmp/signature_validation_XXXXX) sleep 2 echo "$DOWNLOADING" @@ -12,11 +12,11 @@ curl_exit_code=$? sleep 2 if [[ $curl_exit_code -eq 0 ]]; then echo "$DOWNLOADED" - echo -e "${4}" >/tmp/signature_validation/firmware_signature.base64 - echo -e "${5}" >/tmp/signature_validation/firmware_cert.pem - openssl x509 -pubkey -noout -in /tmp/signature_validation/firmware_cert.pem >/tmp/signature_validation/pubkey.pem - openssl base64 -d -in /tmp/signature_validation/firmware_signature.base64 -out /tmp/signature_validation/firmware_signature.sha256 - r=$(openssl dgst -sha256 -verify /tmp/signature_validation/pubkey.pem -signature /tmp/signature_validation/firmware_signature.sha256 "${3}") + echo -e "${4}" >"$SIGNATURE_VALIDATION_DIR/firmware_signature.base64" + echo -e "${5}" >"$SIGNATURE_VALIDATION_DIR/firmware_cert.pem" + openssl x509 -pubkey -noout -in "$SIGNATURE_VALIDATION_DIR/firmware_cert.pem" >"$SIGNATURE_VALIDATION_DIR/pubkey.pem" + openssl base64 -d -in "$SIGNATURE_VALIDATION_DIR/firmware_signature.base64" -out "$SIGNATURE_VALIDATION_DIR/firmware_signature.sha256" + r=$(openssl dgst -sha256 -verify "$SIGNATURE_VALIDATION_DIR/pubkey.pem" -signature "$SIGNATURE_VALIDATION_DIR/firmware_signature.sha256" "${3}") if [ "$r" = "Verified OK" ]; then echo "$SIGNATURE_VERIFIED" @@ -27,4 +27,4 @@ else echo "$DOWNLOAD_FAILED" fi -rm -rf /tmp/signature_validation +rm -rf "$SIGNATURE_VALIDATION_DIR" diff --git a/tests/ocpp_tests/.gitignore b/tests/ocpp_tests/.gitignore new file mode 100644 index 000000000..9cf4be7e8 --- /dev/null +++ b/tests/ocpp_tests/.gitignore @@ -0,0 +1,10 @@ +build +__pycache__ +*.egg-info +.pytest_cache +.venv +results.xml +result.xml +report.html +**/.DS_Store +**/.idea diff --git a/tests/ocpp_tests/README.md b/tests/ocpp_tests/README.md new file mode 100644 index 000000000..8c6c04b4a --- /dev/null +++ b/tests/ocpp_tests/README.md @@ -0,0 +1,69 @@ +# OCPP Integration Tests + +This directory contains some test tooling and integration tests +for OCPP1.6 and OCPP2.0.1. + +## Requirements + +In order to run the integration tests, you need to have everest-core compiled +and installed on your system. Please also make sure to install the python +requirements. + +```bash +cd everest-core/ +cmake -S . -B build -DBUILD_TESTING=ON +cmake --build build --target install --parallel -j$(nproc) +. build/venv/bin/activate +cmake --build build --target everestpy_pip_install_dist +cmake --build build --target everest-testing_pip_install_dist +cmake --build build --target iso15118_pip_install_dist +python3 -m pip install aiofile>=3.7.4 +python3 -m pip install netifaces>=0.11.0 +cd tests/ocpp_tests +python3 -m pip install -r requirements.txt +``` + +## Run the tests + +You can run the integration tests using the convenience scripts +provided in this directory e.g. + +```bash +./run-testing.sh +``` + +This command runs all test cases in parallel. +The time for running the test cases depends on your system. +It usually takes a couple of minutes. +You can check out the test results by opening the generated `results.html`. + +You can choose to run the tests sequentially and/or only run subsets +for OCPP1.6 or OCPP2.0.1 using any of the other run scripts. + +Alternatively, you can run individual test sets or test cases using + +```bash +python3 -m pytest test_sets/ocpp201/remote_control.py \ + --everest-prefix \ + -k 'test_F01_F02_F03' +``` + +e.g. + +```bash +python3 -m pytest test_sets/ocpp201/remote_control.py \ + --everest-prefix ~/checkout/everest-core/build/dist \ + -k 'test_F01_F02_F03' +``` + +This runs test case `test_F01_F02_F03` +specified in `test_sets/ocpp201/remote_control.py`. + +If you run the test cases individually, +make sure to have all required certificates and configs +for the test cases installed using the +convenience scripts inside [test_sets/everest-aux](test_sets/everest-aux/) + +```bash +./install_certs +./install_configs diff --git a/tests/ocpp_tests/conftest.py b/tests/ocpp_tests/conftest.py new file mode 100644 index 000000000..3fed598ba --- /dev/null +++ b/tests/ocpp_tests/conftest.py @@ -0,0 +1,455 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +# noinspection PyUnresolvedReferences +from everest.testing.core_utils.fixtures import * +from everest.testing.core_utils.probe_module import ProbeModule + +# pylint: disable-next=unused-import +from everest.testing.ocpp_utils.fixtures import ( + ocpp_config, + ocpp_version, + charge_point, + charge_point_v201, + central_system, + central_system_v16, + central_system_v201, + central_system_v16_standalone, + test_utility, +) + +import test_sets.everest_test_utils as everest_test_utils + +from typing import Any, Callable + +import logging + + +def pytest_addoption(parser): + parser.addoption( + "--everest-prefix", + action="store", + default="~/checkout/everest-workspace/everest-core", + help="everest-core path; default = '~/checkout/everest-workspace/everest-core'", + ) + + +def pytest_sessionfinish(session, exitstatus): + pass + + +@pytest.fixture +def test_config(request): + return everest_test_utils.test_config(request) + + +@pytest.fixture +def core_config(request) -> EverestEnvironmentCoreConfiguration: + everest_prefix = Path(request.config.getoption("--everest-prefix")) + + marker = request.node.get_closest_marker("everest_core_config") + + if marker is None: + test_function_name = request.function.__name__ + test_module_name = request.module.__name__ + everest_config_path = everest_test_utils.get_everest_config( + test_function_name, test_module_name + ) + else: + everest_config_path = ( + Path(__file__).parent / "test_sets/everest-aux/config" / marker.args[0] + ) + + return EverestEnvironmentCoreConfiguration( + everest_core_path=everest_prefix, + template_everest_config_path=everest_config_path, + ) + + +@pytest.fixture +def started_test_controller(test_controller): + test_controller.start() + yield test_controller + test_controller.stop() + + +@pytest.fixture +def skip_implementation(): + return None + + +@pytest.fixture +def overwrite_implementation(): + return None + + +def implement_command( + module: ProbeModule, + skip_implementation: dict, + implementation_id: str, + command_name: str, + handler: Callable[[dict], Any], +): + skip = False + if skip_implementation: + if implementation_id in skip_implementation: + to_skip = skip_implementation[implementation_id] + if command_name in to_skip: + logging.info(f"Skipping implementation of {command_name}") + skip = True + if not skip: + module.implement_command(implementation_id, command_name, handler) + + +@pytest.fixture +def probe_module( + started_test_controller, everest_core, skip_implementation +) -> ProbeModule: + # initiate the probe module, connecting to the same runtime session the test controller started + module = ProbeModule(everest_core.get_runtime_session()) + + logging.info(f"hello: {skip_implementation}") + + # implement necessary commands for initialization in the module + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "get_evse", + lambda arg: {"id": 1, "connectors": [{"id": 1}]}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "enable_disable", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "authorize_response", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "withdraw_authorization", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "reserve", + lambda arg: False, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "cancel_reservation", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "set_faulted", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "pause_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "resume_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "stop_transaction", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "force_unlock", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "set_get_certificate_response", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorA", + "external_ready_to_start_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "get_evse", + lambda arg: {"id": 2, "connectors": [{"id": 1}]}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "enable_disable", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "authorize_response", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "withdraw_authorization", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "reserve", + lambda arg: False, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "cancel_reservation", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "set_faulted", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "pause_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "resume_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "stop_transaction", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "force_unlock", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "set_get_certificate_response", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleConnectorB", + "external_ready_to_start_charging", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "get_boot_reason", + lambda arg: "PowerUp", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "update_firmware", + lambda arg: "Accepted", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "allow_firmware_installation", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "upload_logs", + lambda arg: "Accepted", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "is_reset_allowed", + lambda arg: True, + ) + implement_command( + module, skip_implementation, "ProbeModuleSystem", "reset", lambda arg: None + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSystem", + "set_system_time", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_leaf_expiry_days_count", + lambda arg: 42, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_v2g_ocsp_request_data", + lambda arg: {"ocsp_request_data_list": []}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_mo_ocsp_request_data", + lambda arg: {"ocsp_request_data_list": []}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "install_ca_certificate", + lambda arg: "Accepted", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "delete_certificate", + lambda arg: "Accepted", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "update_leaf_certificate", + lambda arg: "Accepted", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "verify_certificate", + lambda arg: "Valid", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_installed_certificates", + lambda arg: {"status": "Accepted", "certificate_hash_data_chain": []}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "update_ocsp_cache", + lambda arg: None, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "is_ca_certificate_installed", + lambda arg: False, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "generate_certificate_signing_request", + lambda arg: {"status": "Accepted"}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_leaf_certificate_info", + lambda arg: {"status": "Accepted"}, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_verify_file", + lambda arg: "", + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "verify_file_signature", + lambda arg: True, + ) + implement_command( + module, + skip_implementation, + "ProbeModuleSecurity", + "get_all_valid_certificates_info", + lambda arg: {"status": "NotFound", "info": []}, + ) + + return module + + +@pytest.fixture() +def ocpp_config_reader(ocpp_config, ocpp_configuration): + """ + Returns a reader over the final OCPP config (after all adaptations during test setup) for convenience. + """ + return everest_test_utils.OCPPConfigReader(ocpp_configuration) diff --git a/tests/ocpp_tests/pytest.ini b/tests/ocpp_tests/pytest.ini new file mode 100644 index 000000000..ef18d9c57 --- /dev/null +++ b/tests/ocpp_tests/pytest.ini @@ -0,0 +1,17 @@ +[pytest] +log_cli=true +log_level=DEBUG +asyncio_mode=strict +markers= + ocpp_version: Ocpp version + everest_core_config: Override EVerest config file to use in the test + inject_csms_mock: Inject a unittest.mock into chargepoint methods + probe_module: Enable the use of the probe module in this test. + source_certs_dir: Specify a Path to a directory to copy the initial certificates from + use_temporary_persistent_store: Use a test-local temporary file for the persistent store database + csms_tls: Use a CSMS with TLS + ocpp_config_adaptions: Adaptions to the libocpp configuration + ocpp_config: Select a specific libocpp configuration file + everest_config_adaptions: Adaptions to the EVerest configuration +python_files=test_sets/*.py +pythonpath=test_sets diff --git a/tests/ocpp_tests/requirements.txt b/tests/ocpp_tests/requirements.txt new file mode 100644 index 000000000..2d1ea5579 --- /dev/null +++ b/tests/ocpp_tests/requirements.txt @@ -0,0 +1,12 @@ +pytest +ocpp +python-dateutil +pytest-asyncio +paho-mqtt==1.6.1 +pyftpdlib==2.0.1 +websockets==13.1 +pyOpenSSL +pytest-html +pytest-xdist +cryptography +pytest-timeout diff --git a/tests/ocpp_tests/run-testing-1.6-serial.sh b/tests/ocpp_tests/run-testing-1.6-serial.sh new file mode 100755 index 000000000..cb500e331 --- /dev/null +++ b/tests/ocpp_tests/run-testing-1.6-serial.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running tests serially" + +# run all tests serially +"$PYTHON_INTERPRETER" -m pytest --junitxml=result.xml --html=report.html --self-contained-html test_sets/ocpp16/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/run-testing-1.6.sh b/tests/ocpp_tests/run-testing-1.6.sh new file mode 100755 index 000000000..ebb75a91e --- /dev/null +++ b/tests/ocpp_tests/run-testing-1.6.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running $PARALLEL_TESTS tests in parallel" + +# run all tests in parallel +"$PYTHON_INTERPRETER" -m pytest -d --tx "$PARALLEL_TESTS"*popen//python="$PYTHON_INTERPRETER" -rA --junitxml=result.xml --html=report.html --self-contained-html --max-worker-restart=0 test_sets/ocpp16/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/run-testing-2.0.1-serial.sh b/tests/ocpp_tests/run-testing-2.0.1-serial.sh new file mode 100755 index 000000000..1d09af24c --- /dev/null +++ b/tests/ocpp_tests/run-testing-2.0.1-serial.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running tests serially" + +# run all tests serially +"$PYTHON_INTERPRETER" -m pytest --junitxml=result.xml --html=report.html --self-contained-html test_sets/ocpp201/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/run-testing-2.0.1.sh b/tests/ocpp_tests/run-testing-2.0.1.sh new file mode 100755 index 000000000..24c70fd64 --- /dev/null +++ b/tests/ocpp_tests/run-testing-2.0.1.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running $PARALLEL_TESTS tests in parallel" + +# run all tests in parallel +"$PYTHON_INTERPRETER" -m pytest -d --tx "$PARALLEL_TESTS"*popen//python="$PYTHON_INTERPRETER" -rA --junitxml=result.xml --html=report.html --self-contained-html --max-worker-restart=0 test_sets/ocpp201/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/run-testing-header.sh b/tests/ocpp_tests/run-testing-header.sh new file mode 100755 index 000000000..ca179b58d --- /dev/null +++ b/tests/ocpp_tests/run-testing-header.sh @@ -0,0 +1,22 @@ +#!/bin/bash +PYTHON_INTERPRETER="${PYTHON_INTERPRETER:-python3}" +echo "Using python: $PYTHON_INTERPRETER" +OCPP_TESTING_DIR=$(cd $(dirname "${BASH_SOURCE:-$0}") && pwd) +EVEREST_CORE_DIR=$(dirname $(dirname "$OCPP_TESTING_DIR")) + +if [ ! -d "$EVEREST_CORE_DIR" ]; then + echo "everest-core not found at: $EVEREST_CORE_DIR" + exit 0 +fi + +echo "Using everest-core: $EVEREST_CORE_DIR" + +PARALLEL_TESTS=$(nproc) +if [ $# -eq 1 ] ; then + PARALLEL_TESTS="$1" +fi + +echo "Running $PARALLEL_TESTS tests in parallel" + +cd "$OCPP_TESTING_DIR" +$(cd test_sets/everest-aux/ && ./install_certs.sh "$EVEREST_CORE_DIR/build/dist" && ./install_configs.sh "$EVEREST_CORE_DIR/build/dist") diff --git a/tests/ocpp_tests/run-testing-serial.sh b/tests/ocpp_tests/run-testing-serial.sh new file mode 100755 index 000000000..f4fdf3d83 --- /dev/null +++ b/tests/ocpp_tests/run-testing-serial.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running tests serially" + +# run all tests serially +"$PYTHON_INTERPRETER" -m pytest --junitxml=result.xml --html=report.html test_sets/ocpp16/*.py test_sets/ocpp201/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/run-testing.sh b/tests/ocpp_tests/run-testing.sh new file mode 100755 index 000000000..5eb65f0ff --- /dev/null +++ b/tests/ocpp_tests/run-testing.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +source ./run-testing-header.sh + +echo "Running $PARALLEL_TESTS tests in parallel" + +# run all tests in parallel +"$PYTHON_INTERPRETER" -m pytest -d --tx "$PARALLEL_TESTS"*popen//python="$PYTHON_INTERPRETER" -rA --junitxml=result.xml --html=report.html --self-contained-html --max-worker-restart=0 --timeout=300 --dist loadgroup test_sets/ocpp16/*.py test_sets/ocpp201/*.py --everest-prefix "$EVEREST_CORE_DIR/build/dist" diff --git a/tests/ocpp_tests/test_sets/__init__.py b/tests/ocpp_tests/test_sets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.key b/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.key new file mode 100644 index 000000000..ff597539c --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIQp0AVHO1RxECAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAi8SQDuVzHGLigOLpx49A7BIIE +0I5Z4VrRDtaRX1RiLyn8+XulrA9c3LbcLwLG54fMTLhpoT5D0gUYwlyLXogNQQeK +ad9Y0xfKzHTlWSRxMDuO9AnZ8XGWi8AuezIbBXsmDdkYHxT4RLN/XB86wT2jS53H +1pxzu+dmv1A650rQv2Mo8rOH4vyBiBFiGHNcrBL9JMVu7wRseWxY5riAf/1A/EH9 +eSwvYWNOTuNJBNEJWQo3g97brACMR3CKzvCCXNAyrtk6dBzn81JJCqHskFUAAVf8 +Rb1VrgIth4Oh58bAJvs7A1ZubdDbesMP4GG6uaeJxfgbIyWuIrEBHUNRy8MzDtmu +QLSqJ09enIfEX0TuZaycMFsrWHkyTF4T7A8LmFGRaiBmRNgGHhR+5au+Qp+sFUwp +6cOLAP5Mr/wwhlLFt3CTM3Fe++NGxpd7qp6g0VfhdGDy7dKtgVLwdnWMCm3PT8LS +MsDlsHDl/Dxp33EPHcAHhP+KUFWHL5PZ5aw4J8o4IPNzOd91wyWcg8UCKO2ULQph +dvfu2/7wphv6UVdvjdOmj6a9cqiPE/5gcgsaf7eAkYkhkydi4K5j6Z1LBCx/SJs6 +XMYSRfKOZ8jUSFrimF7wGbnEQj3jt2KRWh9mHI+6YjOT040Rk/Xl9sJ9JXqka++3 +QKdDpMAv7cWtAOYozI3B/MrES75BbmCACds8566rSeC5MSj6y+ed+CclNrV256iR +az8NXaTI/Cd9rz13d7CRjHTXPhYI446b+KiKJ6xpzK+2vQdoYtZRcTSsLkQ43l1+ +cZ1LhzbeTgCy5flizfkv7pDSk84ff81pcJrCctUxHJWwtr5bvSmCam9Flq0LOcMy +6BtXbL7RgTzCb/cdGuHoSSgjhA64qR7iBRlG3plzthbKh1KJpGScgxCkHf8BAEqf +6fzX+X2dpnbzaA+NoQDzuvJGHosZ7lTNuZdAgm2VQWm96YeX0gk/+Pra8fZfc8FY +N22dTHN5YAVh8Y46VaE00Rq+bgcoedZPMgyHrpNnHNojoOsOkoBXRN7OuuGlxuJW +me09XbC1bxdz/OnLZQTLbUBXcVy+rsX0nCFGnBcmkSwsvztejoc1RvHhM6vfAVH7 +pPhyCcmXzM8bu5AtktQ+OI1OcxhAOgONgTbarKR4B0zNfXM0mFrZMbSvhikITvvY +9YSc30LM6ntVDKq0u00v3WMbFsMQhA6npobj8TQRv5/3wtdhbckk7t+NJ4HJjGze +fNWUGayiZfq3TN2bZ4aL373EzvX5F4cyEjJf6EoWOLdKF+sMn65mmvorloQwNjYZ +v7ZKnmOlB+bEiYXs7kLviQacNxHBKlSuVsKhGrDT6nDOBmb8ZfhTzKdG742idYoZ +nLwQdrIDq5Mm8EKR9mqhAORLhQ3fcXVM5/WPzNaZ60pExEjg7XNtdL/VS3h3UfWb +wrrDIggc92yTpL1Py3R+ILCwxPzf9SfDjhHmVblsd1PUAOFgDtMY+5QYgJBd/4fe +N76KCtpbamni6keggi1im9zvt+hHExR/N1hQDT9aT2WoGRsWvlvBtPaXGKwC55o/ +/2D9kFfTmiskLJZyG06N8JjPD/cWZ6/VQsEbufXwvQiBeemvZex51hBRHsIyOYVy +3TobYslI9r1scv7rat2K4zNAMFtbuJdmCrd97i89R6UW +-----END ENCRYPTED PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.pem new file mode 100644 index 000000000..e62da0aeb --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/CSMS_SERVER.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC9DCCAdwCFCIpbxSzML1UlNW/bodFk/00Hfu+MA0GCSqGSIb3DQEBCwUAMDMx +CzAJBgNVBAYTAkRFMQ8wDQYDVQQKDAZQaW9uaXgxEzARBgNVBAMMCkNTTVNSb290 +Q0EwIBcNMjQxMTA2MTAzMDA1WhgPMjEyNDEwMTMxMDMwMDVaMDgxCzAJBgNVBAYT +AkRFMQ8wDQYDVQQKDAZQaW9uaXgxGDAWBgNVBAMMD3Bpb25peC5jc21zLmNvbTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOhTRohwKCuqYsXP0lhVNSNf +G6AbB71SWwD4Sp3fNbMk5i67I0VpSYpf6pS/GoMgcjEiPP0KLirNaMsMedM/RU/a +7jJhjDN3fQPsG+CO2ia6uFkQuntJXMyncQImfxL+ursJR5SB7Q2lh6bRyWxDcXff +wdDL62ZBfjZtTg9ppdB/3BM2Mxd+/Hu1BpiWtn+k73PqWJt0GKa9E+Ue2l2Y0FTh +Bw39LdVn1ZIDgrS5Xe6M/wpG9hMKPqnXYmTnXH1mBM5lukgRGzutP3WrfEdNXX2n +W0tDFqW9Qx6BqgLsvYnWgPq3GMmrNDJ5++/FASyntCQhPSl9Lbjvq3EJQAQvhJkC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAM2tUtCQXSB21LwflnvhDRNrrM5UoCu1B +5qHr1XJaYrH14fgdj24iORLVzecNdU0HS4F7yYP6M67reURntY0Ctaw6u0QbV1wU +5ruaWkSBIYsEc7Tujm8QqVSz4cwvdmzkTTgVFfPRkOpvZ1PgPbq8Q9GyitUEJXuJ +sqB+Q2/JFPwh+6y1TckPq70/gWu0z4CSap9VQU3ed3Fr+RMf9lNh4+q88Mt2tdIV +cvlpQrlneKGbo8mBv9gZ9dOFFjYMWAZWSx8lsJeV6uFlq+7VSGRVmNTva3XAuTcs +WOvMQ5AiSxGVasGIUmbm2mybTMO7eypXXCvKBWidtsBlNnGNqfpNwA== +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.der b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.der new file mode 100644 index 000000000..fa6063a30 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.pem new file mode 100644 index 000000000..a5eb183db --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA1.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB6DCCAY+gAwIBAgICMEUwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSTETMBEG +A1UEAwwKUHJvdlN1YkNBMTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUx +EzARBgoJkiaJk/IsZAEZFgNDUFMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATv +5jV2NbYx1OebIXgjbp+fImlGtDoDaH9plx/+DkjpI5MTb1/SngF5eW7ik0Bk82K2 ++IZ/+IP4vP47GBk67ovAo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQUQJMEvROBa1y6eidJyuk0pnGeNEUwHwYDVR0jBBgw +FoAUZ8ap4nueZjMRXRWNB7elswXz4z4wCgYIKoZIzj0EAwIDRwAwRAIgTBwzZ2ke +NLzUKTaXRItUjIathvG+UGSnMEUYTR0M3XgCIES1rMZ7vw0lDCDZfcs21O6YL+c1 +u319fD6e/O/PWYga +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.der b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.der new file mode 100644 index 000000000..422e1d057 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.pem new file mode 100644 index 000000000..bb6fe3654 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/CPS_SUB_CA2.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB6TCCAZCgAwIBAgICMEYwCgYIKoZIzj0EAwIwSTETMBEGA1UEAwwKUHJvdlN1 +YkNBMTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is +ZAEZFgNDUFMwIBcNMjMwOTI2MDczODM0WhgPMjIyMzA4MDkwNzM4MzRaMEkxEzAR +BgNVBAMMClByb3ZTdWJDQTIxEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRF +MRMwEQYKCZImiZPyLGQBGRYDQ1BTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +MUw55nn9M8smH52TtU8LkM6n8szWVIRAJmz88z1dY5UPrA4Zvd0ad+YdVJRnGUoK +QRLGsqBg0PzPcySqpc/uuKNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFD2NLMFiCvyvVwx6mXCv304tovlQMB8GA1UdIwQY +MBaAFECTBL0TgWtcunonScrpNKZxnjRFMAoGCCqGSM49BAMCA0cAMEQCID2D0Jkb +I+nwsJdMGv0Al0QxnHyRVYfWUmBHiaLpAHiqAiARcpQm91Q8Q7oZQ/S3OFeCnai3 +67cHM5XXmueZ/ZLSXw== +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/INTERMEDIATE_CPS_CA_CERTS.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/INTERMEDIATE_CPS_CA_CERTS.pem new file mode 100644 index 000000000..c0fbfbb8b --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cps/INTERMEDIATE_CPS_CA_CERTS.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIB6TCCAZCgAwIBAgICMEYwCgYIKoZIzj0EAwIwSTETMBEGA1UEAwwKUHJvdlN1 +YkNBMTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is +ZAEZFgNDUFMwIBcNMjMwOTI2MDczODM0WhgPMjIyMzA4MDkwNzM4MzRaMEkxEzAR +BgNVBAMMClByb3ZTdWJDQTIxEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRF +MRMwEQYKCZImiZPyLGQBGRYDQ1BTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +MUw55nn9M8smH52TtU8LkM6n8szWVIRAJmz88z1dY5UPrA4Zvd0ad+YdVJRnGUoK +QRLGsqBg0PzPcySqpc/uuKNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFD2NLMFiCvyvVwx6mXCv304tovlQMB8GA1UdIwQY +MBaAFECTBL0TgWtcunonScrpNKZxnjRFMAoGCCqGSM49BAMCA0cAMEQCID2D0Jkb +I+nwsJdMGv0Al0QxnHyRVYfWUmBHiaLpAHiqAiARcpQm91Q8Q7oZQ/S3OFeCnai3 +67cHM5XXmueZ/ZLSXw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB6DCCAY+gAwIBAgICMEUwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSTETMBEG +A1UEAwwKUHJvdlN1YkNBMTEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUx +EzARBgoJkiaJk/IsZAEZFgNDUFMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATv +5jV2NbYx1OebIXgjbp+fImlGtDoDaH9plx/+DkjpI5MTb1/SngF5eW7ik0Bk82K2 ++IZ/+IP4vP47GBk67ovAo2YwZDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQUQJMEvROBa1y6eidJyuk0pnGeNEUwHwYDVR0jBBgw +FoAUZ8ap4nueZjMRXRWNB7elswXz4z4wCgYIKoZIzj0EAwIDRwAwRAIgTBwzZ2ke +NLzUKTaXRItUjIathvG+UGSnMEUYTR0M3XgCIES1rMZ7vw0lDCDZfcs21O6YL+c1 +u319fD6e/O/PWYga +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.key b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.key new file mode 100644 index 000000000..3f7698832 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDCAs2pQSpksFxt +Xcvt/Fnu+nQ/VxfkhdrPB1plrCOGdy+2HtbZ6hwHeKfdGmcCRt1eAlJPUrgLWiT3 +LIVMjrJ9qTtZOpfhCt0YOSD97ZP7vTVLE7bW/lSsRmuqMsvymxttAk3TIdGQpBQ0 +BZZENhDYCSbbCAcWq948dtXKyNzX0BZdCSDkd1b0AbnY7LE0x0atQZFm9jHr9irb +AgK6QZdYp9HMERewWxgta7jtDCusHDCit5Yt9ahtBfB5pS/pmk5gnRAlYpOEGhlb +CdD5dTKQOLKbHmW94c6p0G7BTOl1EtN2CTW06KZxxZaipVQzqz2WGwGjYmcdnhVQ +MDU0hy+pAgMBAAECggEAR/BcLC9ytcVDcHZAQO26t0d9RWNZA66ylOPIHD05KwoU +0fYbetA5NngB3pWErq5yNQKtXKZyghsZ6+FBSEL9YmUXEZ4NZS/vDaVZW2712Xmu +Qjl8KbpC0WKHV6PgRgRHpiMdknVOzNBagXO05XQayNCT7NHMNxbhoA/8dGYIpajo +qyaJpNx3zin3k87RigBtbLf4w6BWyv6pmzxby4aeiesJtwzx4jf8h6KdwlHpNbrF +unIDlpooRtdx0ALPefyJvnJ/95D4tndGOdgiRnuz9I9VvjsVOfKkzAmWOgC/M0IS +7Z/bE+2URuzu/ry6Urp0U5b+JLstcbRsirdL7qnlxQKBgQDS5lSKGOD8nJYAJiuh +q4kxdmqOGzwgb7u2M5NgkMyi8SUJJZXG7ALoCdQe1OHfAIhwsE6slJYHWpzaTXAN +p2MAbKt5VK6yvq1mUE1A47pi3ihGNctpd3xAw4haP9cDtw1Iqu2yJYo10Vn7QKl2 +cIetJMcUMptAbgT0hL03ZoByNwKBgQDrf+Zgogmy+6giGRUiHENTF7TFQGau1hVN +9jMHVkGDisZC7IInMihwBkXkT5n7LcNkiU8yg0Oj4niAln6bbdxJ8y7CRQEy2g0n +gAZALzPWjQAk9IBCE5m5W58v/qHd0ftSWjtqZ+OYLbUbect6/v+hXFb0J2l3Ln+p +27SoLi/9HwKBgFJfLfPGJdHkYt3qCq6ZbftIsfORBZnxqhJO8KgNxi96GioJaQeJ +1NTGSfhE03ejIKdK5V+YpUR4Cr1k83gRwaQ/zXWVMqqTuOw2PwYyK/FDrd1GU418 +4qX0+QOu3Y8Q5vpT8ITdDq9Ydlmg9s9Qwl1I+QyVe3fdwMe0NKc3vMFfAoGAXzXW +bjsUsMgNsbtyT9gdX/q1mwnuecET2/EtsEmvMv9oKKZ1+GLO9nuSxjtohaR62qqo +2kM3lYp6LYKqrSw9Y6htvx0m3uhJaS7ZWBm9W4CmDkrLj+tcuxPPyBeqWYQLl7/j +RaG64kuYbQNQwOlXcGVkwlEs0oJ6GrI418XUoQECgYEAtUwMgJr5xeqvDqNJ065H +SdRSOewUaUPxOtYITqihj0YR22BSWs3X4PPXGDJ2yyRCuvs0gHpPX/F7cmEjF+N5 +QHb/AlePECiCSb8rcMF4phEXZOE+1poVcPvn5dckNeg2YrSo6WMOS7e7NP6hZcrt ++/FBHitjRK8olut1Y/bz+x4= +-----END PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem new file mode 100644 index 000000000..64165c441 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7zCCAdcCFFnSdVg5iSwTLqqaSa1scWOM6AErMA0GCSqGSIb3DQEBCwUAMDMx +CzAJBgNVBAYTAkRFMQ8wDQYDVQQKDAZQaW9uaXgxEzARBgNVBAMMCkNTTVNSb290 +Q0EwIBcNMjQwOTIwMTMyNTUzWhgPMjA1MjAzMDExMzI1NTNaMDMxCzAJBgNVBAYT +AkRFMQ8wDQYDVQQKDAZQaW9uaXgxEzARBgNVBAMMCkNTTVNSb290Q0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCAs2pQSpksFxtXcvt/Fnu+nQ/Vxfk +hdrPB1plrCOGdy+2HtbZ6hwHeKfdGmcCRt1eAlJPUrgLWiT3LIVMjrJ9qTtZOpfh +Ct0YOSD97ZP7vTVLE7bW/lSsRmuqMsvymxttAk3TIdGQpBQ0BZZENhDYCSbbCAcW +q948dtXKyNzX0BZdCSDkd1b0AbnY7LE0x0atQZFm9jHr9irbAgK6QZdYp9HMERew +Wxgta7jtDCusHDCit5Yt9ahtBfB5pS/pmk5gnRAlYpOEGhlbCdD5dTKQOLKbHmW9 +4c6p0G7BTOl1EtN2CTW06KZxxZaipVQzqz2WGwGjYmcdnhVQMDU0hy+pAgMBAAEw +DQYJKoZIhvcNAQELBQADggEBAI085Iyhy9dLD4Dz5HEyY1sCrWZRcbwScCMOyjkI +yMQbWl3HNrwNvd57L18E/Co61qz8m+ZsvFh7VZMnw/tVxOAyzEyTK+iwsj2XLcs0 +P93LeqNXemmO3OcyOjrjGToOCGTqIJVSrPzPsrTxSLPQyUt0llvfPGF2p9fid9eK +wBc2mE34lfdMl1dfWCDiMk8gngOo5cPOvnGob9Mc2m4U517iGyYbQAe/Ew6r6Mrg +GCh1uUaBIkW9Diiq+1Dox5Hp4jWPoSJ4laoTXk27zRxDmAaVCqCM/CtuZdNws6qA +Pa0mUpan/kSQO+RLbScnbFOE4gfBQJaCgxyeuFMJqRqoVhg= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA1.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA1.pem new file mode 100644 index 000000000..f66451f40 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA1.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgICMDowCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJQ1BPU3ViQ0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMqy +mpvtNjA3+U5TdcucSgdWpXFj8XXwAlb6luBEYCytUD7AREB9P+ksVgcN6GiiZGn8 +0Pdnu+NCuyDLwlUvX6ejgdYwgdMwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFCcnBk2/j/EjG9W6yXgudPVyOgWwMG0GCCsGAQUF +BwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcG +CCsGAQUFBzAChitodHRwczovL3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUt +Q0EuY2VyMB8GA1UdIwQYMBaAFGfGqeJ7nmYzEV0VjQe3pbMF8+M+MAoGCCqGSM49 +BAMCA0gAMEUCICZt4DhW92hiDyUr8oqOUHocKfLRMf5I0vTvajqTbQiVAiEA6as1 +yudx0oHSYf7e7IZBQ6KP1gjC6wcRvfvlBQNbySQ= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA2.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA2.pem new file mode 100644 index 000000000..569921717 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/cso/CPO_SUB_CA2.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgICMDswCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJQ1BPU3Vi +Q0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yMTIzMDkwMjA3MzgzNFowSDESMBAG +A1UEAwwJQ1BPU3ViQ0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEat +pC4ruZ4wc/Hb5JA68ICxU7TQNvLDTJ+Qjc9QetO91h8gAoVRAHKvg8Hoe+lqfu5d ++Q6Ax05xUuFwTzyc3eejgdYwgdMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFDYZY4lJbs1mKm1gGVf3Jw9cDOWPMG0GCCsGAQUF +BwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcG +CCsGAQUFBzAChitodHRwczovL3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUt +Q0EuY2VyMB8GA1UdIwQYMBaAFCcnBk2/j/EjG9W6yXgudPVyOgWwMAoGCCqGSM49 +BAMCA0gAMEUCIQDsQM6q7ecToESugkNzZS3R6il0TKNXeeVgwC84kgb0RAIgfjZh +VXfKo/V7VIHRG9zgM5mO8XdLp+ip25FZbc+V5wU= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mf/MF_ROOT_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mf/MF_ROOT_CA.pem new file mode 100644 index 000000000..2bf7cacf7 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mf/MF_ROOT_CA.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIUefSpxKT9V9AskQWsKw26gKbfryIwDQYJKoZIhvcNAQEL +BQAwZzELMAkGA1UEBhMCREUxCzAJBgNVBAgMAkJXMRMwEQYDVQQHDApIZWlkZWxi +ZXJnMQ8wDQYDVQQKDAZQaW9uaXgxFDASBgNVBAsMC0RldmVsb3BtZW50MQ8wDQYD +VQQDDAZQaW9uaXgwHhcNMjIwNTI1MDgxMjE1WhcNMzIwNTIyMDgxMjE1WjBnMQsw +CQYDVQQGEwJERTELMAkGA1UECAwCQlcxEzARBgNVBAcMCkhlaWRlbGJlcmcxDzAN +BgNVBAoMBlBpb25peDEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxDzANBgNVBAMMBlBp +b25peDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALovuIM86s4FrYts +Ordg6SO9PhTr4Cd9xyux53XttAlCP2GmPC3XWSWUFHj8Mn9UB+8UInvfpIieHCbP +wG/wJGycvIDy2IiteS/bei9H3W25BpDTW7aaIsXauwlGfHJR70GoFXjl3NqrFdeH +IKSPX7haMHDvnTL3YK5d7LdIFPEB8m8rGtYEg7sVN+cqqQDbHNGuDmGto86OIEXh ++mXvDBuoDi3jxCCFaro9FGnE1LddI/FiZvHHPpGvfFFBqQtgXhIc0qdkH6xJL4oY +zvzVlc83wPsTZqmQOiG/+3VCWISLkQRZ94X7SU3KEQ7vTxU7um0O/6NmTOLwEgHY +pNqHNy8CAwEAAaNTMFEwHQYDVR0OBBYEFIsoJXpl8ZCHAGmvi7A2l7ncKjpYMB8G +A1UdIwQYMBaAFIsoJXpl8ZCHAGmvi7A2l7ncKjpYMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQELBQADggEBAHxzfbjod5nzxv0s7PjZ2t5S/RhmW43C6fkveB3o +earwORJaEHY0I8tBizfha39JaF/b1JyGBi4anqluXNRM/1dRXIDxsrIX/z3Un/0f +18wHWZAL5FpG8PqseNFR6zaLYcLIouqRPTLX+rtbQ+l1N+0lAemR4TC7zV+2iyAj +fppq49jwQXZhi7iBotoV4uZJ0ZnWXpFPp67dyRoyAJUKOGVWuKCuQqsWULlkx4i8 +bIW8QQ9uCY/YDUldkONT+LE+uD8inmekaOsxtCkcIv4jKHP3Znxe4iooVqCI/vpn +AL+JtFpWF+lBqjIg7LYKhb4EL41CcF2jp2nsNTEGz4mHZZA= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/INTERMEDIATE_MO_CA_CERTS.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/INTERMEDIATE_MO_CA_CERTS.pem new file mode 100644 index 000000000..5ea64892b --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/INTERMEDIATE_MO_CA_CERTS.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIICdzCCAh2gAwIBAgICMEMwCgYIKoZIzj0EAwIwVzEiMCAGA1UEAwwZUEtJLUV4 +dF9DUlRfTU9fU1VCMV9WQUxJRDEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMC +REUxEjAQBgoJkiaJk/IsZAEZFgJNTzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYy +MTA3MzgzNFowVzEiMCAGA1UEAwwZUEtJLUV4dF9DUlRfTU9fU1VCMl9WQUxJRDEQ +MA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEjAQBgoJkiaJk/IsZAEZFgJN +TzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCy609nf5hPrm5RTmxDGx/NIZBUT +mMjTmzJdeFeNv/KR8vhA7ttt4U71fdkXnV7v9wqhUKzdZ1/aY/UPxdmTYNWjgdYw +gdMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0OBBYE +FA1kQYEMG643y6vTSx9WZwZU5LKZMG0GCCsGAQUFBwEBBGEwXzAkBggrBgEFBQcw +AYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcGCCsGAQUFBzAChitodHRwczov +L3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUtQ0EuY2VyMB8GA1UdIwQYMBaA +FCERWVHh0/KaD6/4zx8a/IFC+bleMAoGCCqGSM49BAMCA0gAMEUCIHyiGWfR0Blg +fBmNz1vgcce+DWlZXhtucfkZnu0iFSKnAiEA24l7RzuuPhEWQVcZiCz4JNYlRQCi +DJMbo6rhh2OkFg4= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICZjCCAgygAwIBAgICMEIwCgYIKoZIzj0EAwIwRjERMA8GA1UEAwwITU9Sb290 +Q0ExEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRFMRIwEAYKCZImiZPyLGQB +GRYCTU8wIBcNMjMwOTI2MDczODM0WhgPMjQyMzA2MjEwNzM4MzRaMFcxIjAgBgNV +BAMMGVBLSS1FeHRfQ1JUX01PX1NVQjFfVkFMSUQxEDAOBgNVBAoMB0VWZXJlc3Qx +CzAJBgNVBAYTAkRFMRIwEAYKCZImiZPyLGQBGRYCTU8wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATCeOBV70uDeFPTzSn/0q/vtTIIUoyi17jtJcBJIJ6HKQ5erQWX +LNHNeWAb67AzhveWaNEidGTCEy8FEfpKQMTJo4HWMIHTMBIGA1UdEwEB/wQIMAYB +Af8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQhEVlR4dPymg+v+M8fGvyB +Qvm5XjBtBggrBgEFBQcBAQRhMF8wJAYIKwYBBQUHMAGGGGh0dHBzOi8vd3d3LmV4 +YW1wbGUuY29tLzA3BggrBgEFBQcwAoYraHR0cHM6Ly93d3cuZXhhbXBsZS5jb20v +SW50ZXJtZWRpYXRlLUNBLmNlcjAfBgNVHSMEGDAWgBTzye4wMVvPhaamN7ESwws6 +ssEXDjAKBggqhkjOPQQDAgNIADBFAiB+nBAlxposIDJxiloT2ELP5+o0MiUTxshl +t3OtZTc7WAIhANJEMAyviGwEpO+EcBFjMKkMUYujjpLQFufl4lnmYIn0 +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_ROOT_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_ROOT_CA.pem new file mode 100644 index 000000000..1fe9c2da3 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_ROOT_CA.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICMTCCAdegAwIBAgICMEEwCgYIKoZIzj0EAwIwRjERMA8GA1UEAwwITU9Sb290 +Q0ExEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRFMRIwEAYKCZImiZPyLGQB +GRYCTU8wIBcNMjMwOTI2MDczODM0WhgPMzAyMzAxMjcwNzM4MzRaMEYxETAPBgNV +BAMMCE1PUm9vdENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTESMBAG +CgmSJomT8ixkARkWAk1PMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOPhLNwtB +1KK0b2zA/+s3UTSeonYiynypWR77zac0/wRBicfWI6BbN5ASCs7AeStsfMclRyzN +/BMTZicBr3hzn6OBsjCBrzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU88nuMDFbz4WmpjexEsMLOrLBFw4wbQYIKwYBBQUHAQEEYTBf +MCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5leGFtcGxlLmNvbS8wNwYIKwYBBQUH +MAKGK2h0dHBzOi8vd3d3LmV4YW1wbGUuY29tL0ludGVybWVkaWF0ZS1DQS5jZXIw +CgYIKoZIzj0EAwIDSAAwRQIhANeKAfZicdBRO4KfW7+E6aPCkyYWPIJzTKqXVvOZ +gVREAiABTYfSqnxXUMkdRWb5ku7gZLdsvFJStRKt1UuQTeOnUQ== +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.der b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.der new file mode 100644 index 000000000..35ab923b7 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.pem new file mode 100644 index 000000000..884b28ad4 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA1.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICZjCCAgygAwIBAgICMEIwCgYIKoZIzj0EAwIwRjERMA8GA1UEAwwITU9Sb290 +Q0ExEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRFMRIwEAYKCZImiZPyLGQB +GRYCTU8wIBcNMjMwOTI2MDczODM0WhgPMjQyMzA2MjEwNzM4MzRaMFcxIjAgBgNV +BAMMGVBLSS1FeHRfQ1JUX01PX1NVQjFfVkFMSUQxEDAOBgNVBAoMB0VWZXJlc3Qx +CzAJBgNVBAYTAkRFMRIwEAYKCZImiZPyLGQBGRYCTU8wWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAATCeOBV70uDeFPTzSn/0q/vtTIIUoyi17jtJcBJIJ6HKQ5erQWX +LNHNeWAb67AzhveWaNEidGTCEy8FEfpKQMTJo4HWMIHTMBIGA1UdEwEB/wQIMAYB +Af8CAQEwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQhEVlR4dPymg+v+M8fGvyB +Qvm5XjBtBggrBgEFBQcBAQRhMF8wJAYIKwYBBQUHMAGGGGh0dHBzOi8vd3d3LmV4 +YW1wbGUuY29tLzA3BggrBgEFBQcwAoYraHR0cHM6Ly93d3cuZXhhbXBsZS5jb20v +SW50ZXJtZWRpYXRlLUNBLmNlcjAfBgNVHSMEGDAWgBTzye4wMVvPhaamN7ESwws6 +ssEXDjAKBggqhkjOPQQDAgNIADBFAiB+nBAlxposIDJxiloT2ELP5+o0MiUTxshl +t3OtZTc7WAIhANJEMAyviGwEpO+EcBFjMKkMUYujjpLQFufl4lnmYIn0 +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.der b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.der new file mode 100644 index 000000000..1c21203da Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.pem new file mode 100644 index 000000000..3a8ddd024 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/mo/MO_SUB_CA2.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICdzCCAh2gAwIBAgICMEMwCgYIKoZIzj0EAwIwVzEiMCAGA1UEAwwZUEtJLUV4 +dF9DUlRfTU9fU1VCMV9WQUxJRDEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMC +REUxEjAQBgoJkiaJk/IsZAEZFgJNTzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYy +MTA3MzgzNFowVzEiMCAGA1UEAwwZUEtJLUV4dF9DUlRfTU9fU1VCMl9WQUxJRDEQ +MA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEjAQBgoJkiaJk/IsZAEZFgJN +TzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCy609nf5hPrm5RTmxDGx/NIZBUT +mMjTmzJdeFeNv/KR8vhA7ttt4U71fdkXnV7v9wqhUKzdZ1/aY/UPxdmTYNWjgdYw +gdMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0OBBYE +FA1kQYEMG643y6vTSx9WZwZU5LKZMG0GCCsGAQUFBwEBBGEwXzAkBggrBgEFBQcw +AYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcGCCsGAQUFBzAChitodHRwczov +L3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUtQ0EuY2VyMB8GA1UdIwQYMBaA +FCERWVHh0/KaD6/4zx8a/IFC+bleMAoGCCqGSM49BAMCA0gAMEUCIHyiGWfR0Blg +fBmNz1vgcce+DWlZXhtucfkZnu0iFSKnAiEA24l7RzuuPhEWQVcZiCz4JNYlRQCi +DJMbo6rhh2OkFg4= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/INTERMEDIATE_OEM_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/INTERMEDIATE_OEM_CA.pem new file mode 100644 index 000000000..9f4b6d26d --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/INTERMEDIATE_OEM_CA.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIB6DCCAY6gAwIBAgICMD8wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNU3Vi +Q0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE+5 +Jw399yjF4tspXmzAomIEET7u6OZ4794J3rmtQBzrwdWi6PXNK1XlwQBw9tgkF1/G +7ASHMNMk02nUQVRoIv2jZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBQ4i1PCQGVLiFD8YkvM3DP3yrGWTTAfBgNVHSMEGDAW +gBRzjJliU3xcjw98B5VT04G6ZyGxDzAKBggqhkjOPQQDAgNIADBFAiEA+UA/zGcv +HttMd1GtcU4IGW78jmP6SlLizNytu3Yg++cCIC0CGpCPsUKPbHBzyCvMwp0DebYL ++atLjhDjPqVGQvYJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB5zCCAY6gAwIBAgICMD4wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGx6 +mv9UeTG4ywVfu1GJ6prtuX7WNbFP377RChPD4sL4TWHldLMKOJu0b0bc2KGWyBu3 +tmq+CbiHJTkEZ+ekEDOjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBRzjJliU3xcjw98B5VT04G6ZyGxDzAfBgNVHSMEGDAW +gBTpHbunA9uW7U/2N8XBh9uc22LiwTAKBggqhkjOPQQDAgNHADBEAiB6OibJal2K +JE1xAU7Wp7K/iDb6XxCkI+EmPd4mE1JG4wIgFbI0VgPlDNioRWfExCqgzMWNeEj+ +xXt2PfIIpifz3Sk= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_CERT_CHAIN.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_CERT_CHAIN.pem new file mode 100644 index 000000000..590de54ba --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_CERT_CHAIN.pem @@ -0,0 +1,39 @@ +-----BEGIN CERTIFICATE----- +MIIB5DCCAYqgAwIBAgICMEAwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNU3Vi +Q0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSjEUMBIG +A1UEAwwLT0VNUHJvdkNlcnQxEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRF +MRMwEQYKCZImiZPyLGQBGRYDT0VNMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +1Qza34iaHRAxMwvGUOTnBvlFicTCFl1cddIvnsd1qbaEyIIRotrOkXhfIQDv4kmi +ue85Cpa2vdn+m1p48W7icaNgMF4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC +A4gwHQYDVR0OBBYEFK5Xv8jMo4+1pvU2GWsZU7BG/kQEMB8GA1UdIwQYMBaAFDiL +U8JAZUuIUPxiS8zcM/fKsZZNMAoGCCqGSM49BAMCA0gAMEUCIQDxjoscE/RMTLZh +9u/ElkpavrVQpkhVmhYOEbQWr/4ijQIgQaHykyPuRZMen3ZCVXqioqsDj6Dq5WAw +Nsf1XdB+Nz8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB6DCCAY6gAwIBAgICMD8wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNU3Vi +Q0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE+5 +Jw399yjF4tspXmzAomIEET7u6OZ4794J3rmtQBzrwdWi6PXNK1XlwQBw9tgkF1/G +7ASHMNMk02nUQVRoIv2jZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBQ4i1PCQGVLiFD8YkvM3DP3yrGWTTAfBgNVHSMEGDAW +gBRzjJliU3xcjw98B5VT04G6ZyGxDzAKBggqhkjOPQQDAgNIADBFAiEA+UA/zGcv +HttMd1GtcU4IGW78jmP6SlLizNytu3Yg++cCIC0CGpCPsUKPbHBzyCvMwp0DebYL ++atLjhDjPqVGQvYJ +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIB5zCCAY6gAwIBAgICMD4wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGx6 +mv9UeTG4ywVfu1GJ6prtuX7WNbFP377RChPD4sL4TWHldLMKOJu0b0bc2KGWyBu3 +tmq+CbiHJTkEZ+ekEDOjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBRzjJliU3xcjw98B5VT04G6ZyGxDzAfBgNVHSMEGDAW +gBTpHbunA9uW7U/2N8XBh9uc22LiwTAKBggqhkjOPQQDAgNHADBEAiB6OibJal2K +JE1xAU7Wp7K/iDb6XxCkI+EmPd4mE1JG4wIgFbI0VgPlDNioRWfExCqgzMWNeEj+ +xXt2PfIIpifz3Sk= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_ROOT_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_ROOT_CA.pem new file mode 100644 index 000000000..ec439b4d9 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_ROOT_CA.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBxTCCAWqgAwIBAgICMD0wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8zMDIzMDEyNzA3MzgzNFowSDESMBAG +A1UEAwwJT0VNUm9vdENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIX6 +D9hpCtQJnHR0+E3EmCsn03Bnx9HxnmFxz8S1i5M6Bp3Poap8Gi12WW06sHAp1UFV +hVzew+MZryodYsO58+6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBTpHbunA9uW7U/2N8XBh9uc22LiwTAKBggqhkjOPQQDAgNJ +ADBGAiEA8bIzMNN3MhUXQvoBTli9wDBJLbr/ZFDFoIhFczKcgdUCIQCaUomBA4Gb +VIGVs3tKXn5XDG1YO2bqNlbycy5Ktb+xVA== +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA1.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA1.pem new file mode 100644 index 000000000..8c37c6ccf --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA1.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB5zCCAY6gAwIBAgICMD4wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGx6 +mv9UeTG4ywVfu1GJ6prtuX7WNbFP377RChPD4sL4TWHldLMKOJu0b0bc2KGWyBu3 +tmq+CbiHJTkEZ+ekEDOjZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBRzjJliU3xcjw98B5VT04G6ZyGxDzAfBgNVHSMEGDAW +gBTpHbunA9uW7U/2N8XBh9uc22LiwTAKBggqhkjOPQQDAgNHADBEAiB6OibJal2K +JE1xAU7Wp7K/iDb6XxCkI+EmPd4mE1JG4wIgFbI0VgPlDNioRWfExCqgzMWNeEj+ +xXt2PfIIpifz3Sk= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA2.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA2.pem new file mode 100644 index 000000000..d87249b3c --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/oem/OEM_SUB_CA2.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB6DCCAY6gAwIBAgICMD8wCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNU3Vi +Q0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJT0VNU3ViQ0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA09FTTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABE+5 +Jw399yjF4tspXmzAomIEET7u6OZ4794J3rmtQBzrwdWi6PXNK1XlwQBw9tgkF1/G +7ASHMNMk02nUQVRoIv2jZjBkMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/ +BAQDAgEGMB0GA1UdDgQWBBQ4i1PCQGVLiFD8YkvM3DP3yrGWTTAfBgNVHSMEGDAW +gBRzjJliU3xcjw98B5VT04G6ZyGxDzAKBggqhkjOPQQDAgNIADBFAiEA+UA/zGcv +HttMd1GtcU4IGW78jmP6SlLizNytu3Yg++cCIC0CGpCPsUKPbHBzyCvMwp0DebYL ++atLjhDjPqVGQvYJ +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.der b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.der new file mode 100644 index 000000000..b726aa9fc Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.pem new file mode 100644 index 000000000..9e49ff3ce --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/ca/v2g/V2G_ROOT_CA.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBxTCCAWqgAwIBAgICMDkwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8zMDIzMDEyNzA3MzgzNFowSDESMBAG +A1UEAwwJVjJHUm9vdENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJjZ +qKsQaffrsSSRTQE57gcpjuxtkKluOMbQWHmpBHgK7coPhm/xlmfDn/rRmQ0fvEqi +zx/oDCt8yAObxSTyj3CjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRnxqnie55mMxFdFY0Ht6WzBfPjPjAKBggqhkjOPQQDAgNJ +ADBGAiEAzmGWz+ES3AskIzWkpyLReF5uumL3P9M6oGbuWQNI7oUCIQCxMh9YfpQ9 +ODORWoaQhzzcGylXRfW0Vo+KbGSUIM5UJQ== +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_CERT_CHAIN.p12 b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_CERT_CHAIN.p12 new file mode 100644 index 000000000..50ecd5be2 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_CERT_CHAIN.p12 differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.der b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.der new file mode 100644 index 000000000..a71d516d9 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.key new file mode 100644 index 000000000..ecda72e53 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,28FD4B20429D2EE2D5A4CD16DC96D9E7 + +6mD0qWchx9nnoG0k6OWhYHAnO/Kt096OWdC2zb7LpxJpPR1QeSLLbGD2C2ZR1HIV +BU6JC5oK4WaLx/n9nN/inyJxnukc+PcsPJfPFMapVB/6cf21TrTQRBo8FXCORzVU +RGbxT7lGQ6N0ygBWy5gen+4Fgvj3ZvCovtHT3E0776Q= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.pem new file mode 100644 index 000000000..ea4da51d7 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB4TCCAYagAwIBAgICMEcwCgYIKoZIzj0EAwIwSTETMBEGA1UEAwwKUHJvdlN1 +YkNBMjEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzARBgoJkiaJk/Is +ZAEZFgNDUFMwHhcNMjMwOTI2MDczODM0WhcNNDgwNTE3MDczODM0WjBHMREwDwYD +VQQDDAhDUFMgTGVhZjEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMCREUxEzAR +BgoJkiaJk/IsZAEZFgNDUFMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASindZ1 +hlVRT/odxEf1LFbYuoTyOh2Oa6CqDX8Um/RSmLG52OVxdKfAGk4R8ORJRNh7QyLd +H09I0ie8IjK4icZeo2AwXjAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAd +BgNVHQ4EFgQUZv5eVYYpEgF/SaUSX3f0y0fHPi4wHwYDVR0jBBgwFoAUPY0swWIK +/K9XDHqZcK/fTi2i+VAwCgYIKoZIzj0EAwIDSQAwRgIhAOyfs/F2IngcG+zT68sb +NyRXTGZSxlwT/lCxM8CyGkR6AiEAo6N6SCi7PLplvLUFqSzZv+71QWiuXptDa+s+ +EWTROjA= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF_PASSWORD.txt b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF_PASSWORD.txt new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_LEAF_PASSWORD.txt @@ -0,0 +1 @@ +123456 diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA1.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA1.key new file mode 100644 index 000000000..67cb9eb33 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA1.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,92A9EC2A77B81ABF10BC8E6AE11B43B6 + +nnH3OCZrUZBdMhocSDduVmuce8nVFaJF4jcq29d+jDABB8ibYppoPHxR6b8+etui +Qhd6iE2TZXtlSctsZvIp4LVh2Tri0WUO678YndrGg06oZgIf+Y8nXyx6G8VyxUGb +QzTtj+wLR1NJVPZtLJcih8GpIHQHUyn5N0c+LzvgoB4= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA2.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA2.key new file mode 100644 index 000000000..80a12ec32 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cps/CPS_SUB_CA2.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,73E1007AF27ADEF53BF5063E7FABAF83 + +Qc1kobAi7yJ3Acx/rsb6+RUE81jv1WY8sFQ172b77P2Yaq7vL+TjWLlChLmFm0No +KiNK+5gY+ylgvItcvrSiCj2UoJgJuHY8MPGGMeVs841VkI8B+cqvnmbfGOcOpl4s +AbTzenCYKABWlsgv+6evQCqHA0DFmFNmH7xbyflh55Q= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.key new file mode 100644 index 000000000..12e5eed0f --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDlaoH7m9gUXbSb +AvKGZr7/9iOLa5pewkPkgKkBzcFW4clJyFfnk8R84JO1zk9AsIhsjs4sDtePl1I2 +cvmKUcZTi07Et/LbIDyur4DFGQIHwDNVHjElWmm6N1SfyKSx4rhInMwMhvx4JBBH +l8OupQdwroIkCsT2OfFoD198+rwzdaPkrE1854+FvMSdKyF+KeLjSG5xcl2MTO9T +YQ8e4Ql1PcSvQHhxuAdG03pLiMiVTCFEFqzu3vd8VqNwbABhVNH6O/SxyUvePuUn +6VCd8//D6fG9FmlTUIMO1C+MTIwU6cKCL0cI/MZo88BjYvzntpxsscut34iR8/WF +S+J/O9HlAgMBAAECggEAFORy5O263mC9CooL3+bqFjGD6Cj+KT4D/jW9uzvR+e5C ++gF3bxzH/cVbJLXrbFoHR1E0AAaNmMNWydc4cXr9lp/u2VkuxS51rqtHjOuFNOmx +SXTTISWcNkireYer5yuqAHbcpqsBjmFeZPhMHXkxXCop3bI0+kvcxIJasSBWblGB +2fCIdgLpmfhbVENE/z1iUDiE2/eEVT17sRAdBjIEDxpMunzLQ4/Hdc7VcKAOjA7y +fjGwGkLzRPkzbLZFFzOrvTkRKOu8bVBH6giN411xxQYIRCpa+BDjb3syoyVHgw6q +o2KYanJ1He41wF0+9o0KlFrz0pXpOgjsYd1vWD3iqwKBgQD42F14RoWI6HbquFQi +wQ6LXcurHT3rcRvHOMUzD3dvelkr7L4thqtYNOSlBM/8QBo1xkMi3k6CgwUuScRi +yHHyMxjXgGsZRcR6ICMDvVVRc+DoC5195OL/HW6PXqD6CNJR5+0NH1JdCruaXzrx +NpcYxfbQFajc+qXcTBoNwvKuDwKBgQDsAyI8l4h2QxQiMalo6XwpoTQfw7p6incQ +EaBzIl+4iDplEu25qo7BvxA5Nfaudy35zaA1hRlKGvcHsxvqlmvoaITC5yrDIqIB +5N6Rgpirie/Wp5Winny8+Iu9aIcUJkqtE1qEQxy4LkFBacz+sxvfDplzJA2Elypp +G75OXEN0ywKBgFIdQ6q+yq3E2AjYTpsxTZVbnCuY+KfKqTnyV9BjmCvnGanO82qe +d8ghnBmAHwnENWHtTJYi+ZFDnuAJY46dSkx75ASo0a6DQTRzilpfjdnU/TBVNOEo +OGeq1KLmvQQFCTIR8D1WSp19Py7Poema8/0uxiUgIJra8wRg8G/+FoqtAoGAbW/i +j0Agyd2+10A58ujZZyBV4CjNLodIQE48HUciJZodocKOMxqwSYzEBBNOyIWA7yV3 +FXobSO6J/6sA1d1cOg9FCG9St9s2TjSHM+ffzSMP8HQTAa4F30ZM3c47XI+I7wpb +XZsVFR51qdRadvwsf1jwtKBSGFpUExsHOqSzrtMCgYAuCoK7JHZUU+U8RqfJkpci +m522Ldhz+rICT/rByItKFpm9WaKwouZfNB2I45kIHvcl7h9RSCPkFcMs2kTjNF1h +AyAlGnawOe8EOzVc2jVR+0PPcgwSoZ9ZHyPDUXdHCdAzdA256z1zipk1rIEqp7ZH +Y2XZai5tF8r3+sAv5Umr1Q== +-----END PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.pem new file mode 100644 index 000000000..638131fb1 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/csms/CSMS_RSA.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC7TCCAdUCFGQ2LxV6E0juGIHth9kSN7+JXgf0MA0GCSqGSIb3DQEBCwUAMDMx +CzAJBgNVBAYTAkRFMQ8wDQYDVQQKDAZQaW9uaXgxEzARBgNVBAMMCkNTTVNSb290 +Q0EwIBcNMjQwOTIwMTMzMzEyWhgPMjA1MjAyMDYxMzMzMTJaMDExCzAJBgNVBAYT +AkRFMQ8wDQYDVQQKDAZQaW9uaXgxETAPBgNVBAMMCENzbXNMZWFmMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5WqB+5vYFF20mwLyhma+//Yji2uaXsJD +5ICpAc3BVuHJSchX55PEfOCTtc5PQLCIbI7OLA7Xj5dSNnL5ilHGU4tOxLfy2yA8 +rq+AxRkCB8AzVR4xJVppujdUn8ikseK4SJzMDIb8eCQQR5fDrqUHcK6CJArE9jnx +aA9ffPq8M3Wj5KxNfOePhbzEnSshfini40hucXJdjEzvU2EPHuEJdT3Er0B4cbgH +RtN6S4jIlUwhRBas7t73fFajcGwAYVTR+jv0sclL3j7lJ+lQnfP/w+nxvRZpU1CD +DtQvjEyMFOnCgi9HCPzGaPPAY2L857acbLHLrd+IkfP1hUvifzvR5QIDAQABMA0G +CSqGSIb3DQEBCwUAA4IBAQA0d5+3ml1bXHbusG8kINGV81sXX6HyusBFPDGYROaW +5HR2CsLPIHdKWn7gyQV9holsI4aB+ZtQ/XVlZmtUTpHZkRFN2SmAs1tXbbQTBsWG +5tVBO1/JtbRwxOsPU249y8xKFCslPCMLgbaw7FBUpFDpHDd2Q2YimqF3VY49cRjf +vwEaWDqmPPPdF3pNtvS5KeiSsAQdQYB4wF26/nO52qAEpt7FaoG8GNUJqLRpLQj3 +/4fWPo7nxdntTKkaushW/XlfbvgS47lgiuQqzyDZF5lC/LLGs0Ml7N/k1nBsg7wZ +0KLKRNKUb01kz/Na6WpkVY/8T9KL1D0mymHhDAaIVrWc +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/CPO_CERT_CHAIN.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/CPO_CERT_CHAIN.pem new file mode 100644 index 000000000..f16cdf294 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/CPO_CERT_CHAIN.pem @@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIB3jCCAYWgAwIBAgICMDwwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJQ1BPU3Vi +Q0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAeFw0yMzA5MjYwNzM4MzRaFw00MDAyMjkwNzM4MzRaMEcxETAPBgNV +BAMMCFNFQ0NDZXJ0MRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEG +CgmSJomT8ixkARkWA0NQTzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKBdMxlw +3aS5nb5nJcL6wrXy7wpHuA1zQUHd4Lu9JjJjsmbFJ1aU/YjeNjd486cBnNFjef2J +k7ugxFPGzgcgCRijYDBeMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgOIMB0G +A1UdDgQWBBTsh2ntDu+kucMCihpJHD7K+ayx2TAfBgNVHSMEGDAWgBQ2GWOJSW7N +ZiptYBlX9ycPXAzljzAKBggqhkjOPQQDAgNHADBEAiBm1ez6tTr5EBCL4lc0GxE2 +gFBov4vf4QbI4V5/a8XlaAIgB+XyVyd20UJsJu6zIZS3mowJ1OMzZ8lWJxXAJznu +hQQ= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgICMDswCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJQ1BPU3Vi +Q0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yMTIzMDkwMjA3MzgzNFowSDESMBAG +A1UEAwwJQ1BPU3ViQ0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEat +pC4ruZ4wc/Hb5JA68ICxU7TQNvLDTJ+Qjc9QetO91h8gAoVRAHKvg8Hoe+lqfu5d ++Q6Ax05xUuFwTzyc3eejgdYwgdMwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFDYZY4lJbs1mKm1gGVf3Jw9cDOWPMG0GCCsGAQUF +BwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcG +CCsGAQUFBzAChitodHRwczovL3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUt +Q0EuY2VyMB8GA1UdIwQYMBaAFCcnBk2/j/EjG9W6yXgudPVyOgWwMAoGCCqGSM49 +BAMCA0gAMEUCIQDsQM6q7ecToESugkNzZS3R6il0TKNXeeVgwC84kgb0RAIgfjZh +VXfKo/V7VIHRG9zgM5mO8XdLp+ip25FZbc+V5wU= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgICMDowCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJVjJHUm9v +dENBMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSDESMBAG +A1UEAwwJQ1BPU3ViQ0ExMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTET +MBEGCgmSJomT8ixkARkWA1YyRzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMqy +mpvtNjA3+U5TdcucSgdWpXFj8XXwAlb6luBEYCytUD7AREB9P+ksVgcN6GiiZGn8 +0Pdnu+NCuyDLwlUvX6ejgdYwgdMwEgYDVR0TAQH/BAgwBgEB/wIBATAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0OBBYEFCcnBk2/j/EjG9W6yXgudPVyOgWwMG0GCCsGAQUF +BwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cHM6Ly93d3cuZXhhbXBsZS5jb20vMDcG +CCsGAQUFBzAChitodHRwczovL3d3dy5leGFtcGxlLmNvbS9JbnRlcm1lZGlhdGUt +Q0EuY2VyMB8GA1UdIwQYMBaAFGfGqeJ7nmYzEV0VjQe3pbMF8+M+MAoGCCqGSM49 +BAMCA0gAMEUCICZt4DhW92hiDyUr8oqOUHocKfLRMf5I0vTvajqTbQiVAiEA6as1 +yudx0oHSYf7e7IZBQ6KP1gjC6wcRvfvlBQNbySQ= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.key new file mode 100644 index 000000000..62d1c202a --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,E286B477F370D35ED00DFB2037181B4E + +WidZNEVp7+k899BFCl9vEI5GtR3xQlHtyRmtUxB26EnHWlNZkNv7WqIZcH0ovLrs +ycR9YteLo6mVW/ecDYkkfiaaog1YOylyxWjYwEB1A6zySU+tav/o6TNqRcynLCpX +ypWR6wIDkjOso56mnD24hT0dFQL94ZCjYHb5d0tNPBs= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.pem new file mode 100644 index 000000000..dc4042b29 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB3jCCAYWgAwIBAgICMDwwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJQ1BPU3Vi +Q0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA1YyRzAeFw0yMzA5MjYwNzM4MzRaFw00MDAyMjkwNzM4MzRaMEcxETAPBgNV +BAMMCFNFQ0NDZXJ0MRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEG +CgmSJomT8ixkARkWA0NQTzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABKBdMxlw +3aS5nb5nJcL6wrXy7wpHuA1zQUHd4Lu9JjJjsmbFJ1aU/YjeNjd486cBnNFjef2J +k7ugxFPGzgcgCRijYDBeMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgOIMB0G +A1UdDgQWBBTsh2ntDu+kucMCihpJHD7K+ayx2TAfBgNVHSMEGDAWgBQ2GWOJSW7N +ZiptYBlX9ycPXAzljzAKBggqhkjOPQQDAgNHADBEAiBm1ez6tTr5EBCL4lc0GxE2 +gFBov4vf4QbI4V5/a8XlaAIgB+XyVyd20UJsJu6zIZS3mowJ1OMzZ8lWJxXAJznu +hQQ= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF_PASSWORD.txt b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF_PASSWORD.txt new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/cso/SECC_LEAF_PASSWORD.txt @@ -0,0 +1 @@ +123456 diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_CERT_CHAIN.p12 b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_CERT_CHAIN.p12 new file mode 100644 index 000000000..db2e3e0a4 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_CERT_CHAIN.p12 differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.der b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.der new file mode 100644 index 000000000..e443ebddc Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.key new file mode 100644 index 000000000..368e24e31 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,524C74F543317C05FACD9FAFCA52345A + +WTtugwCb+B1t64NAsJlhcSJlyWvNfi2/i+X5YjsCoLVksBEdrhaXmNgBNKC2jD3j +y+Y+ljw1pGyAvBhNjHOyCno/0HBZrCSMFXRrwp4g0rqDK16yF/ZjMI9k8F1qtv7m +kfy2xqSLcHYc0+ntlD1mgIWCsnlTejWbsdAl9BFB2ps= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.pem new file mode 100644 index 000000000..6e7a656fc --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICZjCCAg2gAwIBAgICMEQwCgYIKoZIzj0EAwIwVzEiMCAGA1UEAwwZUEtJLUV4 +dF9DUlRfTU9fU1VCMl9WQUxJRDEQMA4GA1UECgwHRVZlcmVzdDELMAkGA1UEBhMC +REUxEjAQBgoJkiaJk/IsZAEZFgJNTzAgFw0yMzA5MjYwNzM4MzRaGA8yMjIzMDgw +OTA3MzgzNFowTTEYMBYGA1UEAwwPVUtTV0kxMjM0NTY3ODlBMRAwDgYDVQQKDAdF +VmVyZXN0MQswCQYDVQQGEwJERTESMBAGCgmSJomT8ixkARkWAk1PMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAE9isd5jdi0yk3WytwQk6YuYRwN0ZaZ/WqRGetcHxi +uHO+xp4cEIMHSLzUgp1FuXm6ypD9SQSPSnj0nGUc1It2ZKOB0DCBzTAMBgNVHRMB +Af8EAjAAMA4GA1UdDwEB/wQEAwID6DAdBgNVHQ4EFgQUTc1VvSACKpeoXyBwGuzU +zZcOQNEwbQYIKwYBBQUHAQEEYTBfMCQGCCsGAQUFBzABhhhodHRwczovL3d3dy5l +eGFtcGxlLmNvbS8wNwYIKwYBBQUHMAKGK2h0dHBzOi8vd3d3LmV4YW1wbGUuY29t +L0ludGVybWVkaWF0ZS1DQS5jZXIwHwYDVR0jBBgwFoAUDWRBgQwbrjfLq9NLH1Zn +BlTkspkwCgYIKoZIzj0EAwIDRwAwRAIgDHw5J2ecr7QCbgfa1EhfueoqYSIVCFBD +Am9629lXT+ACIAVcRW8WgW/ZuR5/wCeejntf2Xg94ywrZuRIVNLFQuVY +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF_PASSWORD.txt b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF_PASSWORD.txt new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_LEAF_PASSWORD.txt @@ -0,0 +1 @@ +123456 diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_ROOT_CA.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_ROOT_CA.key new file mode 100644 index 000000000..f4531f6f1 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_ROOT_CA.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,60045B8B69CF8543559A43B31FD9CA7B + +20P+XV/6fRKTiQ4l0pKeluDw1txdq0IdejYAfAH65MME9NKoSTagLTHRYAEdczJ5 +oQqven3M25hAVN+X0QRUl6ZcbyVThM7U0zL1pFtdG8Rwb3l3tk60qBr4S5yJpDel +U674NwEC5gKNhSQiwiTBij2CJGVidOpKIDT6IYubIK0= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA1.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA1.key new file mode 100644 index 000000000..35ec770b3 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA1.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,AAC5A3AA533020F7E51F4B27F20A57D9 + ++VYyK1UaLoZHmjn9pCKblfjUfp/daTvhH9gnDVlU34gKFOeqs+jqnqhdPXO5bboW +uljn51J17IvJ3Z8K62mQ//t/13f5FXRG/66pukF2/8qRknk9gNswI0FA7g6hndS2 +2fFcJp1kmW8qaS+/uzVQ5+JVthbu37UXyJTgFDakoNk= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA2.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA2.key new file mode 100644 index 000000000..cc80facf6 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/mo/MO_SUB_CA2.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,11DD29A4D8FA7BE406A00F6F753D69F4 + +h94ykrzzFgmAYtzcc2LPIKuyqgyi9fUMTA0bxvQSq/8ftXY+2gC6Rpm5RLyiSFvo +ok71wI//901HIKlY37Qf7BSaGK7hVQDphF2PpyVKm3j/P/ADah8aQBQov4Qb1G0i +tfg5wvk4uTk5qq8ake6npLQ/ub0XVIU03TrJMd6GFHU= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_KEYSTORE.jks b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_KEYSTORE.jks new file mode 100644 index 000000000..91bb45db4 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_KEYSTORE.jks differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_TRUSTSTORE.jks b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_TRUSTSTORE.jks new file mode 100644 index 000000000..c65b0e29d Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/EVCC_TRUSTSTORE.jks differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_CERT_CHAIN.p12 b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_CERT_CHAIN.p12 new file mode 100644 index 000000000..af4fd1692 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_CERT_CHAIN.p12 differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.der b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.der new file mode 100644 index 000000000..650346db2 Binary files /dev/null and b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.der differ diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.key new file mode 100644 index 000000000..e8e7a3541 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,2E1E9BBA33EED7A28A60F6807E4ED742 + +fuKzuGIoh9KQVmL1iYdbxxBfIzcCMdwAkLUSF8m1nLTzW+9mSFrkA3wjRMmjOBbf +LsiSFawr0fOcf1N80jpoc2jsXAyOBcxhL9rucLc11uCFaxNFtowCO+g/B8vqSDYo +Pu/QI3P1c5XNPoELhPGfN6tTCc9dGhXzWMCFSW8aRm4= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.pem b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.pem new file mode 100644 index 000000000..d7c7be3e4 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB5DCCAYqgAwIBAgICMEAwCgYIKoZIzj0EAwIwSDESMBAGA1UEAwwJT0VNU3Vi +Q0EyMRAwDgYDVQQKDAdFVmVyZXN0MQswCQYDVQQGEwJERTETMBEGCgmSJomT8ixk +ARkWA09FTTAgFw0yMzA5MjYwNzM4MzRaGA8yNDIzMDYyMTA3MzgzNFowSjEUMBIG +A1UEAwwLT0VNUHJvdkNlcnQxEDAOBgNVBAoMB0VWZXJlc3QxCzAJBgNVBAYTAkRF +MRMwEQYKCZImiZPyLGQBGRYDT0VNMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +1Qza34iaHRAxMwvGUOTnBvlFicTCFl1cddIvnsd1qbaEyIIRotrOkXhfIQDv4kmi +ue85Cpa2vdn+m1p48W7icaNgMF4wDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC +A4gwHQYDVR0OBBYEFK5Xv8jMo4+1pvU2GWsZU7BG/kQEMB8GA1UdIwQYMBaAFDiL +U8JAZUuIUPxiS8zcM/fKsZZNMAoGCCqGSM49BAMCA0gAMEUCIQDxjoscE/RMTLZh +9u/ElkpavrVQpkhVmhYOEbQWr/4ijQIgQaHykyPuRZMen3ZCVXqioqsDj6Dq5WAw +Nsf1XdB+Nz8= +-----END CERTIFICATE----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF_PASSWORD.txt b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF_PASSWORD.txt new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_LEAF_PASSWORD.txt @@ -0,0 +1 @@ +123456 diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_ROOT_CA.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_ROOT_CA.key new file mode 100644 index 000000000..6d07889a1 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_ROOT_CA.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,68D8ECF4F203CAE620E97FB653C99ED8 + +vf/skaF4qWk9CUE/Ng1axJV1H5TMX4j3+LlQOBEWg8sMgfhmEvSdS0G6TX0vYF+/ +31dz5e9+YIMQDmW2u5uUTM9Gi714TDHG5u284OgmHtAo7fAu+EQ13/uBqJEAs4yl +hEqbosHz6/j/GeiJHAmAq+QO9EG1ebFuMPCb6UFCfjA= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA1.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA1.key new file mode 100644 index 000000000..0315720d3 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA1.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,F7E3640B1C8474AA78E0129947CD6737 + +6TiKhKc0a0UaGxzajYDDk/Zcy8YVZWY2XGSTdlGtpcfTxWyzFWhWoNUce/aEBYOD +x1pZg6gIZTR7KCEt8T9ItMHg0OY6q2Ug+r8UTc0hgkFDUIQ3UQiRNEJh4Ke3Kzra +q9oJ72gO78bfc/zqFvwXb2pyAtr2gkVFuBdjb4SLWgA= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA2.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA2.key new file mode 100644 index 000000000..aa90f571c --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/oem/OEM_SUB_CA2.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,71F99C97FCE7E48910234807353204A9 + +dv2wPXFoffxT5UJhk1r+Owrc7Mm0wR5FUNWTE2RNs8HlWmXHWkyzAk6g6wteh27H +m7FhVpzEK56VvTW+vHgUx6ux6xKz9qJzyBi+AQvyi2Rcf40CarOBqpJahkA2lJ4w +XNyN16eTnjloztq/ZG/lO02++sOQ0VysdF+bzGd/oMA= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA.key b/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA.key new file mode 100644 index 000000000..fb0619d26 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA.key @@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,B8F5097C93B64415AC2940579510AFAA + +rTMIQLlcPnSkAH8ZWNUKCtue0KYoKK+AtaNwQkdenavGpQ5gl3wlzH6hf2pYLVAX +ADUtPwz6WGDMuH1qT9vQ20FdfPde4TIdXbmX1GsIS8VrHh3JRG5gkYnktdki6m4F +u+UTTLgZJ+qcDVA/6InzuBBffUkie91y1T1d3TMw0Kk= +-----END EC PRIVATE KEY----- diff --git a/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA_PASSWORD.txt b/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA_PASSWORD.txt new file mode 100644 index 000000000..9f358a4ad --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/certs/client/v2g/V2G_ROOT_CA_PASSWORD.txt @@ -0,0 +1 @@ +123456 diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-042_1.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-042_1.yaml new file mode 100644 index 000000000..0eb9cad86 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-042_1.yaml @@ -0,0 +1,114 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-042_1.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-078.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-078.yaml new file mode 100644 index 000000000..6f09a7ae1 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-078.yaml @@ -0,0 +1,116 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-078.json + EnableExternalWebsocketControl: true + UserConfigPath: user_config.json + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: ca/csms/CSMS_ROOT_CA.pem + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp16-probe-module.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp16-probe-module.yaml new file mode 100644 index 000000000..f261405b0 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp16-probe-module.yaml @@ -0,0 +1,27 @@ +active_modules: + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: probe + implementation_id: evse_manager + - module_id: probe + implementation_id: evse_manager_b + reservation: + - module_id: probe + implementation_id: reservation + auth: + - module_id: probe + implementation_id: auth + system: + - module_id: probe + implementation_id: system + security: + - module_id: probe + implementation_id: security +x-module-layout: {} + diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-data-transfer.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-data-transfer.yaml new file mode 100644 index 000000000..c69de6cff --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-data-transfer.yaml @@ -0,0 +1,126 @@ +active_modules: + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: "1" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + connector_2: + module: EvseManager + config_module: + connector_id: 2 + has_ventilation: true + evse_id: "2" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_2 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_2 + implementation_id: powermeter + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + yeti_driver_2: + module: JsYetiSimulator + config_module: + connector_id: 2 + ocpp: + module: OCPP201 + config_module: + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + persistent_store: + module: PersistentStore + config_module: + sqlite_db_file_path: persistent_store.db + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: "ca/csms/CSMS_ROOT_CA.pem" + csms_leaf_cert_directory: "client/csms" + csms_leaf_key_directory: "client/csms" + mf_ca_bundle: "ca/mf/MF_ROOT_CA.pem" + mo_ca_bundle: "ca/mo/MO_ROOT_CA.pem" + v2g_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + secc_leaf_cert_directory: "client/cso" + secc_leaf_key_directory: "client/cso" + private_key_password: "123456" + auth: + module: Auth + config_module: + connection_timeout: 30 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: ocpp + implementation_id: auth_provider + - module_id: token_provider_manual + implementation_id: main + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + - module_id: connector_2 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-dir-bundles.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-dir-bundles.yaml new file mode 100644 index 000000000..43648def1 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-dir-bundles.yaml @@ -0,0 +1,186 @@ +active_modules: + iso15118_charger: + module: PyJosev + config_module: + device: auto + supported_DIN70121: false + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: "1" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + connector_2: + module: EvseManager + config_module: + connector_id: 2 + has_ventilation: true + evse_id: "2" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_2 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_2 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + yeti_driver_2: + module: JsYetiSimulator + config_module: + connector_id: 2 + slac: + module: JsSlacSimulator + car_simulator_1: + module: JsCarSimulator + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + simulation_control: + - module_id: yeti_driver_1 + implementation_id: yeti_simulation_control + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + car_simulator_2: + module: JsCarSimulator + config_module: + connector_id: 2 + auto_enable: true + auto_exec: false + connections: + simulation_control: + - module_id: yeti_driver_2 + implementation_id: yeti_simulation_control + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP201 + config_module: + ChargePointConfigPath: config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + persistent_store: + module: PersistentStore + config_module: + sqlite_db_file_path: persistent_store.db + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: "ca/csms" + csms_leaf_cert_directory: "client/csms" + csms_leaf_key_directory: "client/csms" + mf_ca_bundle: "ca/mf" + mo_ca_bundle: "ca/mo/MO_ROOT_CA.pem" # use a file for the MO bundle since that folder in the test data contains certs for the car simulator as well + v2g_ca_bundle: "ca/v2g" + secc_leaf_cert_directory: "client/cso" + secc_leaf_key_directory: "client/cso" + private_key_password: "123456" + auth: + module: Auth + config_module: + connection_timeout: 30 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: ocpp + implementation_id: auth_provider + - module_id: token_provider_manual + implementation_id: main + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + - module_id: connector_2 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module-data-transfer.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module-data-transfer.yaml new file mode 100644 index 000000000..b9b76933d --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module-data-transfer.yaml @@ -0,0 +1,25 @@ +active_modules: + ocpp: + module: OCPP201 + config_module: + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: probe + implementation_id: ProbeModuleConnectorA + - module_id: probe + implementation_id: ProbeModuleConnectorB + auth: + - module_id: auth + implementation_id: main + system: + - module_id: probe + implementation_id: ProbeModuleSystem + security: + - module_id: probe + implementation_id: ProbeModuleSecurity + data_transfer: + - module_id: probe + implementation_id: ProbeModuleDataTransfer +x-module-layout: {} + diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module.yaml new file mode 100644 index 000000000..af26a4c7d --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-probe-module.yaml @@ -0,0 +1,39 @@ +active_modules: + ocpp: + module: OCPP201 + config_module: + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: probe + implementation_id: ProbeModuleConnectorA + - module_id: probe + implementation_id: ProbeModuleConnectorB + auth: + - module_id: auth + implementation_id: main + system: + - module_id: probe + implementation_id: ProbeModuleSystem + security: + - module_id: probe + implementation_id: ProbeModuleSecurity + auth: + module: Auth + config_module: + connection_timeout: 30 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: probe + implementation_id: ProbeModuleConnectorA + - module_id: probe + implementation_id: ProbeModuleConnectorB +x-module-layout: {} + diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201.yaml new file mode 100644 index 000000000..193907883 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201.yaml @@ -0,0 +1,189 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: allow + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: "1" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + connector_2: + module: EvseManager + config_module: + connector_id: 2 + has_ventilation: true + evse_id: "2" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + connections: + bsp: + - module_id: yeti_driver_2 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_2 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + yeti_driver_2: + module: JsYetiSimulator + config_module: + connector_id: 2 + slac: + module: JsSlacSimulator + car_simulator_1: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver_1 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + car_simulator_2: + module: JsEvManager + config_module: + connector_id: 2 + auto_enable: true + auto_exec: false + connections: + ev_board_support: + - module_id: yeti_driver_2 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP201 + config_module: + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + persistent_store: + module: PersistentStore + config_module: + sqlite_db_file_path: persistent_store.db + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: "ca/csms/CSMS_ROOT_CA.pem" + csms_leaf_cert_directory: "client/csms" + csms_leaf_key_directory: "client/csms" + mf_ca_bundle: "ca/mf/MF_ROOT_CA.pem" + mo_ca_bundle: "ca/mo/MO_ROOT_CA.pem" + v2g_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + secc_leaf_cert_directory: "client/cso" + secc_leaf_key_directory: "client/cso" + private_key_password: "123456" + auth: + module: Auth + config_module: + connection_timeout: 30 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: ocpp + implementation_id: auth_provider + - module_id: token_provider_manual + implementation_id: main + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + - module_id: connector_2 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-1.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-1.yaml new file mode 100644 index 000000000..a3a02e6ce --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-1.yaml @@ -0,0 +1,116 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test-security-profile-1.json + EnableExternalWebsocketControl: true + UserConfigPath: user_config.json + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: ca/csms/CSMS_ROOT_CA.pem + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-2.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-2.yaml new file mode 100644 index 000000000..c7bb3f2e4 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-security-profile-2.yaml @@ -0,0 +1,116 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test-security-profile-2.json + EnableExternalWebsocketControl: true + UserConfigPath: user_config.json + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: ca/csms/CSMS_ROOT_CA.pem + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso no-tls.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso no-tls.yaml new file mode 100644 index 000000000..83072b6eb --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso no-tls.yaml @@ -0,0 +1,144 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: allow + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_ISO15118_2: true + tls_active: false + is_cert_install_needed: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + enable_autocharge: true + evse_id: "DE*PNX*100001" + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + ac_hlc_enabled: true + ac_hlc_use_5percent: false + ac_enforce_hlc: false + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator_1: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver_1 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 60 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider_1 + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + - module_id: connector_1 + implementation_id: token_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-iso-pnc.json + UserConfigPath: user_config.json + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + mf_ca_bundle: "ca/mf/MF_ROOT_CA.pem" + mo_ca_bundle: "ca/mo/MO_ROOT_CA.pem" + v2g_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + csms_leaf_cert_directory: "client/csms" + csms_leaf_key_directory: "client/csms" + secc_leaf_cert_directory: "client/cso" + secc_leaf_key_directory: "client/cso" + private_key_password: "123456" + token_provider_1: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso.yaml new file mode 100644 index 000000000..d549654ef --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-iso.yaml @@ -0,0 +1,144 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: allow + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_ISO15118_2: true + tls_active: true + is_cert_install_needed: true + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: "DE*PNX*100001" + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + ac_hlc_enabled: true + ac_hlc_use_5percent: false + ac_enforce_hlc: false + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator_1: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver_1 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 60 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider_1 + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + - module_id: connector_1 + implementation_id: token_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-iso-pnc.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + config_module: + csms_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + mf_ca_bundle: "ca/mf/MF_ROOT_CA.pem" + mo_ca_bundle: "ca/mo/MO_ROOT_CA.pem" + v2g_ca_bundle: "ca/v2g/V2G_ROOT_CA.pem" + csms_leaf_cert_directory: "client/csms" + csms_leaf_key_directory: "client/csms" + secc_leaf_cert_directory: "client/cso" + secc_leaf_key_directory: "client/cso" + private_key_password: "123456" + token_provider_1: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp-probe.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp-probe.yaml new file mode 100644 index 000000000..7ec8b8065 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp-probe.yaml @@ -0,0 +1,126 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + probe_module: + module: PyProbeModule + connections: + test_control: + - module_id: car_simulator + implementation_id: main + connector_1: + - module_id: connector_1 + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: main + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp.yaml new file mode 100644 index 000000000..692e72143 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-sil-ocpp.yaml @@ -0,0 +1,120 @@ +active_modules: + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + mapping: + module: + evse: 1 + config_module: + connector_id: 1 + has_ventilation: true + evse_id: '1' + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver + implementation_id: powermeter + yeti_driver: + mapping: + module: + evse: 1 + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + auth: + module: Auth + config_module: + connection_timeout: 20 + connections: + token_provider: + - module_id: token_provider_manual + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + token_provider_manual: + module: DummyTokenProviderManual + connections: {} + config_implementation: + main: + token: '123' + type: dummy + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-connectors.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-connectors.yaml new file mode 100644 index 000000000..91831ff99 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-connectors.yaml @@ -0,0 +1,182 @@ +active_modules: + iso15118_charger: + module: EvseV2G + config_module: + device: auto + tls_security: allow + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: "1" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + connector_2: + module: EvseManager + config_module: + connector_id: 2 + has_ventilation: true + evse_id: "2" + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_2 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_2 + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + yeti_driver_2: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + car_simulator_1: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver_1 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + car_simulator_2: + module: JsEvManager + config_module: + connector_id: 2 + auto_enable: true + auto_exec: false + connections: + ev_board_support: + - module_id: yeti_driver_2 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider_1 + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test.json + UserConfigPath: user_config.json + EnableExternalWebsocketControl: true + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + token_provider_1: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + - module_id: connector_2 + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_1 + implementation_id: powermeter + system: + module: System + +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-evse-dc.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-evse-dc.yaml new file mode 100644 index 000000000..3f5568f55 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-two-evse-dc.yaml @@ -0,0 +1,175 @@ +active_modules: + iso15118_charger_1: + module: PyJosev + config_module: + device: auto + supported_DIN70121: true + iso15118_car: + module: PyEvJosev + config_module: + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_AC: false + supported_ISO15118_20_DC: false + connector_1: + module: EvseManager + config_module: + connector_id: 1 + has_ventilation: true + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + charge_mode: DC + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_1 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_1 + implementation_id: powermeter + slac: + - module_id: slac_1 + implementation_id: evse + hlc: + - module_id: iso15118_charger_1 + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + connector_2: + module: EvseManager + config_module: + connector_id: 2 + has_ventilation: true + evse_id: DE*PNX*E12345*2 + session_logging: true + session_logging_xml: false + ac_hlc_enabled: false + ac_hlc_use_5percent: false + ac_enforce_hlc: false + external_ready_to_start_charging: true + connections: + bsp: + - module_id: yeti_driver_2 + implementation_id: board_support + powermeter_grid_side: + - module_id: yeti_driver_2 + implementation_id: powermeter + yeti_driver_1: + module: JsYetiSimulator + config_module: + connector_id: 1 + yeti_driver_2: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac_1: + module: JsSlacSimulator + powersupply_dc: + module: JsDCSupplySimulator + imd: + module: JsIMDSimulator + car_simulator_1: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug + connections: + ev_board_support: + - module_id: yeti_driver_1 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + car_simulator_2: + module: JsEvManager + config_module: + connector_id: 2 + auto_enable: true + auto_exec: false + connections: + ev_board_support: + - module_id: yeti_driver_2 + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider_1 + implementation_id: main + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + token_provider_1: + module: DummyTokenProviderManual + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: connector_1 + implementation_id: energy_grid + - module_id: connector_2 + implementation_id: energy_grid + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-test.json + EnableExternalWebsocketControl: true + UserConfigPath: user_config.json + connections: + evse_manager: + - module_id: connector_1 + implementation_id: evse + - module_id: connector_2 + implementation_id: evse + reservation: + - module_id: auth + implementation_id: reservation + auth: + - module_id: auth + implementation_id: main + system: + - module_id: system + implementation_id: main + security: + - module_id: evse_security + implementation_id: main + evse_security: + module: EvseSecurity + system: + module: System +x-module-layout: {} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-042_1.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-042_1.json new file mode 100644 index 000000000..87289d93d --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-042_1.json @@ -0,0 +1,48 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1" + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": true, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 10, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,SmartCharging", + "TransactionMessageAttempts": 1, + "TransactionMessageRetryInterval": 10, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 0 + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-078.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-078.json new file mode 100644 index 000000000..e0cd517a7 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-078.json @@ -0,0 +1,58 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1" + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": true, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 10, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "TransactionMessageAttempts": 1, + "TransactionMessageRetryInterval": 10, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 0, + "CpoName": "Pionix", + "AdditionalRootCertificateCheck": false + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "Reservation": { + "ReserveConnectorZeroSupported": true + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-iso-pnc.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-iso-pnc.json new file mode 100644 index 000000000..fd3621f2d --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-iso-pnc.json @@ -0,0 +1,64 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "SECCCert", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "LogMessages": true + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": false, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 30, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "TransactionMessageAttempts": 3, + "TransactionMessageRetryInterval": 1, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 0, + "CpoName": "Pionix", + "AdditionalRootCertificateCheck": false + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "Reservation": { + "ReserveConnectorZeroSupported": true + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + }, + "PnC": { + "ISO15118PnCEnabled": true, + "ContractValidationOffline": true, + "CentralContractValidationAllowed": true + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-1.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-1.json new file mode 100644 index 000000000..5779c6bbd --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-1.json @@ -0,0 +1,57 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "LogMessages": true + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": true, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 10, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "TransactionMessageAttempts": 1, + "TransactionMessageRetryInterval": 10, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 1 + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "Reservation": { + "ReserveConnectorZeroSupported": false + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-2.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-2.json new file mode 100644 index 000000000..7fe2d1412 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test-security-profile-2.json @@ -0,0 +1,59 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "LogMessages": true, + "UseSslDefaultVerifyPaths": false + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": true, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 10, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "TransactionMessageAttempts": 1, + "TransactionMessageRetryInterval": 10, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 2, + "AdditionalRootCertificateCheck": false + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "Reservation": { + "ReserveConnectorZeroSupported": false + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test.json new file mode 100644 index 000000000..955109876 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-test.json @@ -0,0 +1,59 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "LogMessages": true + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizeRemoteTxRequests": true, + "AuthorizationCacheEnabled": true, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 10, + "ConnectorPhaseRotation": "0.RST,1.RST", + "GetConfigurationMaxKeys": 100, + "HeartbeatInterval": 86400, + "LocalAuthorizeOffline": false, + "LocalPreAuthorize": false, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValueSampleInterval": 0, + "NumberOfConnectors": 1, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnSampledData": "Energy.Active.Import.Register", + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "TransactionMessageAttempts": 5, + "TransactionMessageRetryInterval": 1, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 0, + "CpoName": "Pionix", + "AdditionalRootCertificateCheck": true + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "Reservation": { + "ReserveConnectorZeroSupported": true + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 42, + "ChargingScheduleAllowedChargingRateUnit": "Current,Power", + "ChargingScheduleMaxPeriods": 42, + "MaxChargingProfilesInstalled": 42 + } +} diff --git a/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx b/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx new file mode 100644 index 000000000..8a25361c6 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx @@ -0,0 +1 @@ +This is a firmware update file diff --git a/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx.base64 b/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx.base64 new file mode 100644 index 000000000..eb9a3aea7 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/firmware/firmware_update.pnx.base64 @@ -0,0 +1,6 @@ +dGdE6NN9ZiReWDzB8kQaEGjEYJzwh0FOPfbjJ3jwc1VeV3wtJzOTfPaUhBEmSd/K +Erb+GOsP74otgm00/pv/pZQ+0nNfd+ZGrPfZK/RsNFJosM3CGAS1w55+tqUVRyhC +CZ4l1GCAzpdStn8c90gyD1IyF6LccMw6Odwq7XDYyIYPMZigZ8fJYKKiQxVYVf7i +BGSsmbG655OKKSS5JX1nE5i0gZt0ZuMaAjQOoA4etu8rXI0KRBueQbmHk1oNscYl +eT50T0JZfSQ2M0pZrzjcSuCIgWYf6L6uGP7tMvA/m/KGu/ufSXwt4hFdbYxp+ofk +1RmNOPjhPnT2XuvsT12UbA== diff --git a/tests/ocpp_tests/test_sets/everest-aux/install_certs.sh b/tests/ocpp_tests/test_sets/everest-aux/install_certs.sh new file mode 100755 index 000000000..dd67c2a1c --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/install_certs.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +usage() { + echo "Usage: $0 " + exit 1 +} + +if [ $# -ne 1 ] ; then + usage +else + EVEREST_CERTS_PATH="$1/etc/everest/certs" + rm -rf "$EVEREST_CERTS_PATH" + mkdir "$EVEREST_CERTS_PATH" + + cp -r certs/ca "$EVEREST_CERTS_PATH" + cp -r certs/client "$EVEREST_CERTS_PATH" +fi diff --git a/tests/ocpp_tests/test_sets/everest-aux/install_configs.sh b/tests/ocpp_tests/test_sets/everest-aux/install_configs.sh new file mode 100755 index 000000000..3d3d79d0f --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/install_configs.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +usage() { + echo "Usage: $0 " + exit 1 +} + +if [ $# -ne 1 ] ; then + usage +else + EVEREST_OCPP_CONFIGS_PATH="$1/share/everest/modules/OCPP" + mkdir -p "$EVEREST_OCPP_CONFIGS_PATH" + + cp config/libocpp-config-* "$EVEREST_OCPP_CONFIGS_PATH" +fi diff --git a/tests/ocpp_tests/test_sets/everest_test_utils.py b/tests/ocpp_tests/test_sets/everest_test_utils.py new file mode 100644 index 000000000..019274f1e --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest_test_utils.py @@ -0,0 +1,641 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from __future__ import annotations + +import asyncio +import hashlib +import queue +import os +from pathlib import Path +import threading +from types import FunctionType +from typing import Optional + +from OpenSSL import crypto +from datetime import datetime, timedelta + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.x509 import load_pem_x509_certificate + +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP201ConfigAdjustment, + OCPP201ConfigVariableIdentifier, +) + +from iso15118.shared.security import ( + CertPath, + KeyEncoding, + KeyPasswordPath, + KeyPath, + create_signature, + encrypt_priv_key, + get_cert_cn, + load_cert, + load_priv_key, +) +from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2 +from iso15118.shared.messages.iso15118_2.header import MessageHeader as MessageHeaderV2 +from iso15118.shared.messages.iso15118_2.datatypes import ( + EMAID, + CertificateChain, + DHPublicKey, + EncryptedPrivateKey, + ResponseCode, + SubCertificates, +) +from iso15118.shared.messages.iso15118_2.body import Body, CertificateInstallationRes +from iso15118.shared.messages.enums import Namespace +from iso15118.shared.exi_codec import EXI +from iso15118.shared.exificient_exi_codec import ExificientEXICodec +from iso15118.shared.exceptions import EncryptionError, PrivateKeyReadError +import json +import base64 + +from everest.testing.ocpp_utils.charge_point_utils import ( + OcppTestConfiguration, + ChargePointInfo, + CertificateInfo, + FirmwareInfo, + AuthorizationInfo, +) + +from ocpp.charge_point import snake_to_camel_case, asdict, remove_nones +from ocpp.v16 import call, call_result +from ocpp.v16.enums import Action, DataTransferStatus +from ocpp.routing import on + +# for OCPP1.6 PnC whitepaper: +from ocpp.v201 import call_result as call_result201 +from ocpp.v201.datatypes import IdTokenInfoType +from ocpp.v201.enums import ( + AuthorizationStatusType, + GenericStatusType, + Iso15118EVCertificateStatusType, + GetCertificateStatusType, +) + + +class EXIGenerator: + + def __init__(self, certs_path): + self.certs_path = certs_path + EXI().set_exi_codec(ExificientEXICodec()) + + def generate_certificate_installation_res( + self, base64_encoded_cert_installation_req: str, namespace: str + ) -> str: + + cert_install_req_exi = base64.b64decode(base64_encoded_cert_installation_req) + cert_install_req = EXI().from_exi(cert_install_req_exi, namespace) + try: + dh_pub_key, encrypted_priv_key_bytes = encrypt_priv_key( + oem_prov_cert=load_cert( + os.path.join(self.certs_path, CertPath.OEM_LEAF_DER) + ), + priv_key_to_encrypt=load_priv_key( + os.path.join(self.certs_path, KeyPath.CONTRACT_LEAF_PEM), + KeyEncoding.PEM, + os.path.join( + self.certs_path, KeyPasswordPath.CONTRACT_LEAF_KEY_PASSWORD + ), + ), + ) + except EncryptionError: + raise EncryptionError( + "EncryptionError while trying to encrypt the private key for the " + "contract certificate" + ) + except PrivateKeyReadError as exc: + raise PrivateKeyReadError( + f"Can't read private key to encrypt for CertificateInstallationRes:" + f" {exc}" + ) + + # The elements that need to be part of the signature + contract_cert_chain = CertificateChain( + id="id1", + certificate=load_cert( + os.path.join(self.certs_path, CertPath.CONTRACT_LEAF_DER) + ), + sub_certificates=SubCertificates( + certificates=[ + load_cert(os.path.join(self.certs_path, CertPath.MO_SUB_CA2_DER)), + load_cert(os.path.join(self.certs_path, CertPath.MO_SUB_CA1_DER)), + ] + ), + ) + encrypted_priv_key = EncryptedPrivateKey( + id="id2", value=encrypted_priv_key_bytes + ) + dh_public_key = DHPublicKey(id="id3", value=dh_pub_key) + emaid = EMAID( + id="id4", + value=get_cert_cn( + load_cert(os.path.join(self.certs_path, CertPath.CONTRACT_LEAF_DER)) + ), + ) + cps_certificate_chain = CertificateChain( + certificate=load_cert(os.path.join(self.certs_path, CertPath.CPS_LEAF_DER)), + sub_certificates=SubCertificates( + certificates=[ + load_cert(os.path.join(self.certs_path, CertPath.CPS_SUB_CA2_DER)), + load_cert(os.path.join(self.certs_path, CertPath.CPS_SUB_CA1_DER)), + ] + ), + ) + + cert_install_res = CertificateInstallationRes( + response_code=ResponseCode.OK, + cps_cert_chain=cps_certificate_chain, + contract_cert_chain=contract_cert_chain, + encrypted_private_key=encrypted_priv_key, + dh_public_key=dh_public_key, + emaid=emaid, + ) + + try: + # Elements to sign, containing its id and the exi encoded stream + contract_cert_tuple = ( + cert_install_res.contract_cert_chain.id, + EXI().to_exi( + cert_install_res.contract_cert_chain, Namespace.ISO_V2_MSG_DEF + ), + ) + encrypted_priv_key_tuple = ( + cert_install_res.encrypted_private_key.id, + EXI().to_exi( + cert_install_res.encrypted_private_key, Namespace.ISO_V2_MSG_DEF + ), + ) + dh_public_key_tuple = ( + cert_install_res.dh_public_key.id, + EXI().to_exi(cert_install_res.dh_public_key, Namespace.ISO_V2_MSG_DEF), + ) + emaid_tuple = ( + cert_install_res.emaid.id, + EXI().to_exi(cert_install_res.emaid, Namespace.ISO_V2_MSG_DEF), + ) + + elements_to_sign = [ + contract_cert_tuple, + encrypted_priv_key_tuple, + dh_public_key_tuple, + emaid_tuple, + ] + # The private key to be used for the signature + signature_key = load_priv_key( + os.path.join(self.certs_path, KeyPath.CPS_LEAF_PEM), + KeyEncoding.PEM, + os.path.join(self.certs_path, KeyPasswordPath.CPS_LEAF_KEY_PASSWORD), + ) + + signature = create_signature(elements_to_sign, signature_key) + + except PrivateKeyReadError as exc: + raise Exception( + "Can't read private key needed to create signature " + f"for CertificateInstallationRes: {exc}", + ) + except Exception as exc: + raise Exception(f"Error creating signature {exc}") + + header = MessageHeaderV2( + session_id=cert_install_req.header.session_id, + signature=signature, + ) + body = Body.parse_obj({"CertificateInstallationRes": cert_install_res.dict()}) + to_be_exi_encoded = V2GMessageV2(header=header, body=body) + exi_encoded_cert_installation_res = EXI().to_exi( + to_be_exi_encoded, Namespace.ISO_V2_MSG_DEF + ) + + base64_encode_cert_install_res = base64.b64encode( + exi_encoded_cert_installation_res + ).decode("utf-8") + + return base64_encode_cert_install_res + + +def certificate_signed_response(csr: crypto.X509Req): + certs_path: str = Path(__file__).parent.resolve() / "everest-aux/certs/" + ca_cert_file = certs_path / "ca/v2g/V2G_ROOT_CA.pem" + ca_key_file = certs_path / "client/v2g/V2G_ROOT_CA.key" + + with open(ca_cert_file, "rb") as ca_cert_file, open( + ca_key_file, "rb" + ) as ca_key_file: + ca_cert_data = ca_cert_file.read() + ca_key_data = ca_key_file.read() + + ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, ca_cert_data) + ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, ca_key_data, b"123456") + + signed_cert = crypto.X509() + signed_cert.set_version(3) + signed_cert.set_serial_number(1) + + signed_cert.set_subject(csr.get_subject()) + signed_cert.set_issuer(ca_cert.get_subject()) + signed_cert.set_pubkey(csr.get_pubkey()) + + validity_days = 365 + not_before = datetime.utcnow() + not_after = not_before + timedelta(days=validity_days) + + signed_cert.set_notBefore(not_before.strftime("%Y%m%d%H%M%SZ").encode("utf-8")) + signed_cert.set_notAfter(not_after.strftime("%Y%m%d%H%M%SZ").encode("utf-8")) + + signed_cert.sign(ca_key, "sha256") + + return crypto.dump_certificate(crypto.FILETYPE_PEM, signed_cert).decode("utf-8") + + +def on_data_transfer(accept_pnc_authorize, **kwargs): + req = call.DataTransferPayload(**kwargs) + if req.vendor_id == "org.openchargealliance.iso15118pnc": + if req.message_id == "Authorize": + if accept_pnc_authorize: + status = AuthorizationStatusType.accepted + else: + status = AuthorizationStatusType.invalid + response = call_result201.AuthorizePayload( + id_token_info=IdTokenInfoType(status=status) + ) + return call_result.DataTransferPayload( + status=DataTransferStatus.accepted, + data=json.dumps(remove_nones(snake_to_camel_case(asdict(response)))), + ) + # Should not be part of DataTransfer.req from CP->CSMS + elif req.message_id == "CertificateSigned": + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + # Should not be part of DataTransfer.req from CP->CSMS + elif req.message_id == "DeleteCertificate": + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + elif req.message_id == "Get15118EVCertificate": + certs_path: str = Path(__file__).parent.resolve() / "everest-aux/certs/" + generator: EXIGenerator = EXIGenerator(certs_path) + exi_request = json.loads(kwargs["data"])["exiRequest"] + namespace = json.loads(kwargs["data"])["iso15118SchemaVersion"] + return call_result.DataTransferPayload( + status=DataTransferStatus.accepted, + data=json.dumps( + remove_nones( + snake_to_camel_case( + asdict( + call_result201.Get15118EVCertificatePayload( + status=Iso15118EVCertificateStatusType.accepted, + exi_response=generator.generate_certificate_installation_res( + exi_request, namespace + ), + ) + ) + ) + ) + ), + ) + elif req.message_id == "GetCertificateStatus": + return call_result.DataTransferPayload( + status=DataTransferStatus.accepted, + data=json.dumps( + remove_nones( + snake_to_camel_case( + asdict( + call_result201.GetCertificateStatusPayload( + status=GetCertificateStatusType.accepted, + ocsp_result="anwfdiefnwenfinfinef", + ) + ) + ) + ) + ), + ) + # Should not be part of DataTransfer.req from CP->CSMS + elif req.message_id == "InstallCertificate": + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + elif req.message_id == "SignCertificate": + return call_result.DataTransferPayload( + status=DataTransferStatus.accepted, + data=json.dumps( + asdict( + call_result201.SignCertificatePayload( + status=GenericStatusType.accepted + ) + ) + ), + ) + # Should not be part of DataTransfer.req from CP->CSMS + elif req.message_id == "TriggerMessage": + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + else: + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + else: + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_vendor_id, data="Please implement me" + ) + + +@on(Action.DataTransfer) +def on_data_transfer_accept_authorize(**kwargs): + return on_data_transfer(accept_pnc_authorize=True, **kwargs) + + +@on(Action.DataTransfer) +def on_data_transfer_reject_authorize(**kwargs): + return on_data_transfer(accept_pnc_authorize=False, **kwargs) + + +def get_everest_config_path_str(config_name): + return (Path(__file__).parent / "everest-aux" / "config" / config_name).as_posix() + + +def get_everest_config(function_name, module_name): + if module_name == "plug_and_charge_tests": + return Path(__file__).parent / Path( + "everest-aux/config/everest-config-sil-iso.yaml" + ) + elif module_name in [ + "provisioning", + "authorization", + "remote_control", + "security", + "local_authorization_list", + "transactions", + "meterValues", + ]: + return Path(__file__).parent / Path( + "everest-aux/config/everest-config-ocpp201.yaml" + ) + else: + return Path(__file__).parent / Path( + "everest-aux/config/everest-config-sil-ocpp.yaml" + ) + + +def test_config(request): + data = json.loads((Path(__file__).parent / "test_config.json").read_text()) + + ocpp_test_config = OcppTestConfiguration( + charge_point_info=ChargePointInfo(**data["charge_point_info"]), + authorization_info=AuthorizationInfo(**data["authorization_info"]), + certificate_info=CertificateInfo(**data["certificate_info"]), + firmware_info=FirmwareInfo(**data["firmware_info"]), + ) + + ocpp_test_config.certificate_info.csms_cert = ( + Path(__file__).parent / ocpp_test_config.certificate_info.csms_cert + ) + ocpp_test_config.certificate_info.csms_key = ( + Path(__file__).parent / ocpp_test_config.certificate_info.csms_key + ) + ocpp_test_config.certificate_info.csms_root_ca = ( + Path(__file__).parent / ocpp_test_config.certificate_info.csms_root_ca + ) + ocpp_test_config.certificate_info.csms_root_ca_invalid = ( + Path(__file__).parent / ocpp_test_config.certificate_info.csms_root_ca_invalid + ) + ocpp_test_config.certificate_info.csms_root_ca_key = ( + Path(__file__).parent / ocpp_test_config.certificate_info.csms_root_ca_key + ) + ocpp_test_config.certificate_info.mf_root_ca = ( + Path(__file__).parent / ocpp_test_config.certificate_info.mf_root_ca + ) + + ocpp_test_config.firmware_info.update_file = ( + Path(__file__).parent / ocpp_test_config.firmware_info.update_file + ) + ocpp_test_config.firmware_info.update_file_signature = ( + Path(__file__).parent / ocpp_test_config.firmware_info.update_file_signature + ) + + return ocpp_test_config + + +async def call_test_function_and_wait(test_function: FunctionType, timeout=20) -> bool: + q = queue.Queue() + + def tst(q): + res = test_function(timeout) + q.put(res) + + test_thread = threading.Thread(target=tst, kwargs={"q": q}) + test_thread.start() + + result = False + while q.empty(): + await asyncio.sleep(1) + + result = q.get() + + return result + + +class CertificateHashDataGenerator: + """ + Compute the hash values for certificates. + + Note: EVSE Security uses the X509_pubkey_digest OpenSSL function for this. + + The hashes are not generated from the whole DER-encoded "Subject Public Key Information" + field, but rather only from the bit-string representing the actual key bits (without the ASN.1 + tag and length). + + Unfortunately, there doesn't seem to be a generic method for + doing this, so RSA and ECDSA keys are handled differently. + If we need to add support for Ed25519 or others, we'd need to + extend the logic here as well. + + Cf: + - https://groups.google.com/g/mailing.openssl.users/c/1hhY2uECxsc + - https://github.com/openssl/openssl/issues/8777 + - https://datatracker.ietf.org/doc/html/rfc5480 + + """ + + @staticmethod + def _sha256(b: bytes) -> str: + return hashlib.sha256(b).hexdigest() + + @classmethod + def get_hash_data( + cls, certificate_path: Path, issuer_certificate_path: Optional[Path] = None + ): + issuer_certificate_path = ( + issuer_certificate_path if issuer_certificate_path else certificate_path + ) + + certificate = load_pem_x509_certificate( + certificate_path.read_bytes(), default_backend() + ) + issuer_certificate = load_pem_x509_certificate( + issuer_certificate_path.read_bytes(), default_backend() + ) + + issuer_name_hash = cls._get_name_hash(issuer_certificate) + issuer_key_hash = cls._get_public_key_hash(issuer_certificate_path) + + assert issuer_name_hash == cls._get_issuer_name_hash(certificate) + + return { + "hash_algorithm": "SHA256", + "issuer_key_hash": issuer_key_hash, + "issuer_name_hash": issuer_name_hash, + "serial_number": hex(certificate.serial_number)[2:].lower(), + # strip 0x according to OCPP spec (CertificateHashDataType) + } + + @classmethod + def _get_public_key_hash(cls, file: Path): + certificate = load_pem_x509_certificate(file.read_bytes(), default_backend()) + # Get the raw key bytes - the method to do this differs by key type + # try RSA + try: + return cls._sha256( + certificate.public_key().public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.PKCS1, + ) + ) + # try ECDSA (Note: We assume we're working with the uncompressed-point format here) + except Exception: + return cls._sha256( + certificate.public_key().public_bytes( + encoding=serialization.Encoding.X962, + format=serialization.PublicFormat.UncompressedPoint, + ) + ) + # if ECDSA also fails, then we need to adjust this method to add more options (e.g. Ed25519) + + @classmethod + def _get_name_hash(cls, certificate: x509.Certificate): + return cls._sha256(certificate.subject.public_bytes()) + + @classmethod + def _get_issuer_name_hash(cls, certificate: x509.Certificate): + return cls._sha256(certificate.issuer.public_bytes()) + + +class CertificateHelper: + + @staticmethod + def _verify_private_key_matches_cert(private_key: crypto.PKey, cert: crypto.X509): + cert_public_key = ( + cert.get_pubkey() + .to_cryptography_key() + .public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ) + pkey_public_key = ( + private_key.to_cryptography_key() + .public_key() + .public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + ) + + assert ( + cert_public_key == pkey_public_key + ), f"Private key is for {pkey_public_key}; certificat has public key {pkey_public_key}" + + @classmethod + def generate_certificate_request( + cls, common_name: str, passphrase: str | bytes | None = None + ) -> tuple[str, str]: + """ + Returns: tuple of certificate request and private key + """ + + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, 2048) + req = crypto.X509Req() + req.get_subject().CN = common_name + req.set_pubkey(key) + req.get_subject().C = "DE" + req.sign(key, "sha256") + csr_data = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) + private_key = crypto.dump_privatekey( + crypto.FILETYPE_PEM, + pkey=key, + cipher="aes256" if passphrase else None, + passphrase=( + passphrase.encode("utf-8") + if isinstance(passphrase, str) + else passphrase + ), + ) + return csr_data.decode("utf-8"), private_key.decode("utf-8") + + @classmethod + def sign_certificate_request( + cls, + csr_data: str | bytes, + issuer_certificate_path: Path, + issuer_private_key_path: Path, + issuer_private_key_passphrase: str | bytes | None = None, + relative_valid_time: int = 0, + relative_expiration_time: int = 9999999, + serial: int = 42, + ) -> str: + + if isinstance(issuer_private_key_passphrase, str): + issuer_private_key_passphrase = issuer_private_key_passphrase.encode( + "utf-8" + ) + if isinstance(csr_data, str): + csr_data = csr_data.encode("utf-8") + + issuer_private_key = crypto.load_privatekey( + crypto.FILETYPE_PEM, + issuer_private_key_path.read_bytes(), + passphrase=issuer_private_key_passphrase, + ) + issuer_cert = crypto.load_certificate( + crypto.FILETYPE_PEM, issuer_certificate_path.read_bytes() + ) + + cls._verify_private_key_matches_cert(issuer_private_key, issuer_cert) + + csr = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_data) + + # Create a new certificate + cert = crypto.X509() + cert.set_subject(csr.get_subject()) + cert.set_pubkey(csr.get_pubkey()) + cert.gmtime_adj_notBefore( + min(relative_valid_time, relative_expiration_time - 1) + ) + cert.gmtime_adj_notAfter(relative_expiration_time) + cert.set_issuer(issuer_cert.get_subject()) + cert.set_serial_number(serial) + cert.sign(issuer_private_key, "SHA256") + signed_certificate = crypto.dump_certificate(crypto.FILETYPE_PEM, cert) + + return signed_certificate.decode(encoding="utf-8") + + +class OCPPConfigReader: + + def __init__(self, config): + self._config_json = config + + def get_variable(self, section: str, variable: str): + identifier = OCPP201ConfigVariableIdentifier(section, variable) + + return GenericOCPP201ConfigAdjustment._get_value_from_v201_config( + self._config_json, identifier + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/authorization_tests.py b/tests/ocpp_tests/test_sets/ocpp16/authorization_tests.py new file mode 100644 index 000000000..41de0d1e5 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/authorization_tests.py @@ -0,0 +1,1316 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +from datetime import datetime, timedelta +import logging +import asyncio + +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +from ocpp.routing import on, create_route_map +from ocpp.v16.datatypes import ( + IdTagInfo, +) +from ocpp.v16 import call, call_result +from ocpp.v16.enums import * + +# fmt: off +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest_test_utils import * +from validations import (validate_standard_start_transaction, + validate_standard_stop_transaction, + validate_remote_start_stop_transaction, + ) +# fmt: on + + +@pytest.mark.asyncio +async def test_authorize_parent_id_1( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + # authorize.conf with parent id tag + + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_authorize_invalid( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo(status=AuthorizationStatus.invalid) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req(key="HeartbeatInterval", value="5") + + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.invalid_id_tag), + ) + + test_utility.messages.clear() + + test_utility.forbidden_actions.append("StatusNotification") + test_utility.forbidden_actions.append("StartTransaction") + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_parent_id_tag_reservation_1( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + parent_id_tag=test_config.authorization_info.parent_id_tag, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_parent_id_tag_reservation_2( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + parent_id_tag=test_config.authorization_info.parent_id_tag, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_2, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_parent_id_tag_reservation_3( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + parent_id_tag=test_config.authorization_info.parent_id_tag, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + test_controller.plug_in() + + await asyncio.sleep(1) + + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(1) + + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + + await asyncio.sleep(1) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.status_notification, connector_id=1 + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + +@pytest.mark.asyncio +async def test_authorization_cache_entry_1( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + @on(Action.StartTransaction) + def on_start_transaction(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.valid_id_tag_1, + ) + return call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=id_tag_info + ) + + setattr(charge_point_v16, "on_authorize", on_authorize) + setattr(charge_point_v16, "on_start_transaction", on_start_transaction) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="false" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + await charge_point_v16.get_configuration_req(key=["LocalAuthListEnabled"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "LocalAuthListEnabled", "readonly": False, "value": "true"}] + ), + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_utility.messages.clear() + + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe card and authorize this by cache + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + test_controller.plug_in() + + await asyncio.sleep(5) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + await asyncio.sleep(2) + + # wait for reconnect + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.status_notification, connector_id=1 + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # swipe card + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_authorization_cache_entry_2( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo(status=AuthorizationStatus.accepted) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + @on(Action.StartTransaction) + def on_start_transaction(**kwargs): + id_tag_info = IdTagInfo(status=AuthorizationStatus.accepted) + return call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=id_tag_info + ) + + setattr(charge_point_v16, "on_authorize", on_authorize) + setattr(charge_point_v16, "on_start_transaction", on_start_transaction) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="false" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + await charge_point_v16.get_configuration_req(key=["LocalAuthListEnabled"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "LocalAuthListEnabled", "readonly": False, "value": "true"}] + ), + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_utility.messages.clear() + test_utility.forbidden_actions.append("Authorize") + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_authorization_cache_entry_3( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + @on(Action.Authorize) + def on_authorize(**kwargs): + # accepted but expires just now + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + expiry_date=datetime.utcnow().isoformat(), + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + @on(Action.StartTransaction) + def on_start_transaction(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + expiry_date=datetime.utcnow().isoformat(), + ) + return call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=id_tag_info + ) + + setattr(charge_point_v16, "on_authorize", on_authorize) + setattr(charge_point_v16, "on_start_transaction", on_start_transaction) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="false" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + await charge_point_v16.get_configuration_req(key=["LocalAuthListEnabled"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "LocalAuthListEnabled", "readonly": False, "value": "true"}] + ), + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_utility.messages.clear() + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_swipe_on_finishing( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + test_controller.plug_in() + await asyncio.sleep(1) + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + await asyncio.sleep(1) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + await asyncio.sleep(2) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_utility.messages.clear() + test_utility.forbidden_actions.append("StopTransaction") + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-two-connectors.yaml") +) +@pytest.mark.asyncio +async def test_remote_start_transaction_no_connector( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + test_controller.plug_in(connector_id=1) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + test_utility.forbidden_actions.append("StopTransaction") + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_2 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + test_controller.plug_in(connector_id=2) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 2, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-two-connectors.yaml") +) +@pytest.mark.asyncio +async def test_remote_start_transaction_single_connector( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=2 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + test_controller.plug_in(connector_id=1) + + await asyncio.sleep(1) + + test_controller.plug_in(connector_id=2) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 2, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 2, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-two-connectors.yaml") +) +@pytest.mark.asyncio +async def test_double_remote_start_transaction( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + test_controller.plug_in(connector_id=1) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + test_utility.forbidden_actions.append("StopTransaction") + + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + test_controller.plug_in(connector_id=2) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 2, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/booting.py b/tests/ocpp_tests/test_sets/ocpp16/booting.py new file mode 100644 index 000000000..25e2d48e5 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/booting.py @@ -0,0 +1,586 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +# fmt: off +from ocpp.routing import create_route_map, on +from ocpp.v16.enums import * +from ocpp.v16 import call +from datetime import datetime +import asyncio +import logging +import pytest +from validations import (validate_standard_start_transaction, + validate_standard_stop_transaction, + validate_boot_notification + ) +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest.testing.ocpp_utils.central_system import CentralSystem +from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP16ConfigAdjustment +from everest_test_utils import * +# fmt: on + + +@pytest.mark.asyncio +async def test_stop_pending_transactions( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, + central_system_v16: CentralSystem, +): + logging.info("######### test_stop_pending_transactions #########") + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # charge for some time... + logging.debug("Charging for a while...") + await asyncio.sleep(2) + + test_controller.stop() + + await asyncio.sleep(2) + + test_controller.start() + + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + await asyncio.sleep(2) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.power_loss), + validate_standard_stop_transaction, + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-1.yaml") +) +@pytest.mark.asyncio +async def test_change_authorization_key_in_pending( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_change_authorization_key_in_pending #########") + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.pending, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + response = await charge_point_v16.get_configuration_req() + assert len(response.configuration_key) > 20 + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + await charge_point_v16.change_configuration_req( + key="AuthorizationKey", value="DEADBEEFDEADBEEF" + ) + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + charge_point_v16 = central_system_v16.chargepoint + + setattr(charge_point_v16, "on_boot_notification", on_boot_notification_accepted) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-1.yaml") +) +@pytest.mark.asyncio +async def test_remote_start_stop_in_pending( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_change_authorization_key_in_pending #########") + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.pending, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + await charge_point_v16.remote_start_transaction_req(id_tag="DEADBEEF") + assert await wait_for_and_validate( + test_utility, charge_point_v16, "RemoteStartTransaction", {"status": "Rejected"} + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=20) + assert await wait_for_and_validate( + test_utility, charge_point_v16, "RemoteStopTransaction", {"status": "Rejected"} + ) + + +@pytest.mark.asyncio +async def test_boot_notification_rejected( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_boot_notification_rejected #########") + + @on(Action.BootNotification) + def on_boot_notification_rejected(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.rejected, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_rejected) + ) + + test_controller.start() + charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + setattr(charge_point_v16, "on_boot_notification", on_boot_notification_accepted) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_boot_notification_callerror( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_boot_notification_callerror #########") + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + # Provoke a CALLERROR as a response to a BootNotification.req + central_system_v16.function_overrides.append(("on_boot_notification", None)) + + test_controller.start() + charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + setattr(charge_point_v16, "on_boot_notification", on_boot_notification_accepted) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + timeout=100, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_boot_notification_no_response( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_boot_notification_no_response #########") + + async def route_message(msg): + return + + # do not respond at all + central_system_v16.function_overrides.append(("route_message", route_message)) + + test_controller.start() + charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + # this is the second BootNotification.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + timeout=100, + ) + + +@pytest.mark.asyncio +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-2.yaml") +) +@pytest.mark.source_certs_dir(Path(__file__).parent / "../everest-aux/certs") +@pytest.mark.asyncio +@pytest.mark.csms_tls +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Internal", "VerifyCsmsCommonName", False)]) +) +async def test_initiate_message_in_pending( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_initiate_message_in_pending #########") + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.pending, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_utility.forbidden_actions.append("SecurityEventNotification") + + test_controller.start() + charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + await charge_point_v16.change_configuration_req(key="CpoName", value="VENID") + + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.status_notification + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_utility.messages.clear() + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.boot_notification + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + test_utility.messages.clear() + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.heartbeat + ) + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + test_utility.messages.clear() + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.diagnostics_status_notification + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.idle), + ) + + test_utility.messages.clear() + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.firmware_status_notification + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.FirmwareStatusNotificationPayload(FirmwareStatus.idle), + ) + + test_utility.messages.clear() + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.status_notification + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.sign_charge_point_certificate + ) + # expect ExtendedTriggerMessage.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ExtendedTriggerMessage", + call_result.ExtendedTriggerMessagePayload(TriggerMessageStatus.accepted), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "SignCertificate", {} + ) + + setattr(charge_point_v16, "on_boot_notification", on_boot_notification_accepted) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + test_utility.forbidden_actions.clear() + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SecurityEventNotification", + {"type": "StartupOfTheDevice"}, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_boot_notification_rejected_and_call_by_csms( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + @on(Action.BootNotification) + def on_boot_notification_rejected(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.rejected, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_rejected) + ) + + test_controller.start() + charge_point_v16: ChargePoint16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + # Response to this message is not allowed + test_utility.forbidden_actions.append("RemoteStartTransaction") + + t = threading.Thread( + target=asyncio.run, + args=( + charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ), + ), + ) + t.start() + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "BootNotification", {} + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/broken.py b/tests/ocpp_tests/test_sets/ocpp16/broken.py new file mode 100644 index 000000000..374989411 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/broken.py @@ -0,0 +1,391 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +import logging +from datetime import datetime + +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +from everest.testing.core_utils.everest_core import EverestCore, Requirement +from everest.testing.core_utils.probe_module import ProbeModule +from ocpp.v16 import call, call_result +from ocpp.v16.enums import * +from ocpp.v16.datatypes import IdTagInfo +from ocpp.messages import Call, _DecimalEncoder +from ocpp.charge_point import snake_to_camel_case +from ocpp.routing import on, create_route_map + +# fmt: off +from validations import wait_for_callerror_and_validate, validate_boot_notification +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate +from everest_test_utils import * +# fmt: on + + +async def send_message_without_validation(charge_point_v16, call_msg): + json_data = json.dumps( + [ + call_msg.message_type_id, + call_msg.unique_id, + call_msg.action, + call_msg.payload, + ], + # By default json.dumps() adds a white space after every separator. + # By setting the separator manually that can be avoided. + separators=(",", ":"), + cls=_DecimalEncoder, + ) + + async with charge_point_v16._call_lock: + await charge_point_v16._send(json_data) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.asyncio +async def test_missing_payload_field( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_missing_payload_field #########") + + payload = call.ChangeConfigurationPayload(key="WebSocketPingInterval", value="0") + camel_case_payload = snake_to_camel_case(asdict(payload)) + + call_msg = Call( + unique_id=str(charge_point_v16._unique_id_generator()), + action=payload.__class__.__name__[:-7], + payload=remove_nones(camel_case_payload), + ) + + # remove a required payload field + del call_msg.payload["value"] + + await send_message_without_validation(charge_point_v16, call_msg) + + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v16, "FormationViolation" + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.skip(reason="libocpp currently does not support this") +@pytest.mark.asyncio +async def test_additional_payload_field( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_additional_payload_field #########") + + payload = call.ChangeConfigurationPayload(key="WebSocketPingInterval", value="0") + camel_case_payload = snake_to_camel_case(asdict(payload)) + + call_msg = Call( + unique_id=str(charge_point_v16._unique_id_generator()), + action=payload.__class__.__name__[:-7], + payload=remove_nones(camel_case_payload), + ) + + # add a payload field + call_msg.payload["additional"] = "123" + + await send_message_without_validation(charge_point_v16, call_msg) + + # FIXME: this message seems to be accepted, should be rejected according to spec... + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v16, "FormationViolation" + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.asyncio +async def test_wrong_payload_type( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_wrong_payload_type #########") + + # key should just be string, but here we set it to array of string + payload = call.ChangeConfigurationPayload(key=["WebSocketPingInterval"], value="0") + camel_case_payload = snake_to_camel_case(asdict(payload)) + + call_msg = Call( + unique_id=str(charge_point_v16._unique_id_generator()), + action=payload.__class__.__name__[:-7], + payload=remove_nones(camel_case_payload), + ) + + await send_message_without_validation(charge_point_v16, call_msg) + + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v16, "FormationViolation" + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.asyncio +async def test_wrong_auth_payload( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_wrong_auth_payload #########") + + @on(Action.Authorize) + def on_authorize(**kwargs): + # send an empty id_tag_info, this should not crash EVerest + id_tag_info = {} + res = call_result.AuthorizePayload(id_tag_info=id_tag_info) + return res + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + charge_point_v16.route_map[Action.Authorize]["_skip_schema_validation"] = True + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # this only works if we don't crash from the broken response + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.probe_module( + connections={"ocpp_data_transfer": [Requirement("ocpp", "data_transfer")]} +) +@pytest.mark.inject_csms_mock +@pytest.mark.asyncio +async def test_data_transfer_with_probe_module( + central_system_v16_standalone: CentralSystem, everest_core: EverestCore +): + logging.info("######### test_data_transfer_with_probe_module #########") + + @on(Action.DataTransfer) + def on_data_transfer(**kwargs): + logging.info(f"Received a data transfer message {datetime.now()}") + req = call.DataTransferPayload(**kwargs) + if req.vendor_id == "PIONIX" and req.message_id == "test_message": + return call_result.DataTransferPayload( + status=DataTransferStatus.accepted, data="Hello there" + ) + elif req.vendor_id == "PIONIX" and req.message_id == "test_message_broken": + # purposefully return a wrong payload + return call_result.AuthorizePayload(id_tag_info={}) + return call_result.DataTransferPayload( + status=DataTransferStatus.unknown_message_id, data="Please implement me" + ) + + cs = central_system_v16_standalone.mock + cs.on_data_transfer.side_effect = on_data_transfer + + probe_module = ProbeModule(everest_core.get_runtime_session()) + probe_module.start() + + await probe_module.wait_to_be_ready() + + charge_point_v16 = await central_system_v16_standalone.wait_for_chargepoint() + charge_point_v16.route_map[Action.DataTransfer]["_skip_schema_validation"] = True + + result = await probe_module.call_command( + "ocpp_data_transfer", + "data_transfer", + { + "request": { + "vendor_id": "PIONIX", + "message_id": "test_message", + "data": "test", + } + }, + ) + assert "data" in result and "status" in result and result["status"] == "Accepted" + + result = await probe_module.call_command( + "ocpp_data_transfer", + "data_transfer", + { + "request": { + "vendor_id": "PIONIX", + "message_id": "test_message_unknown", + "data": "test", + } + }, + ) + assert "status" in result and result["status"] == "UnknownMessageId" + + result = await probe_module.call_command( + "ocpp_data_transfer", + "data_transfer", + { + "request": { + "vendor_id": "PIONIX", + "message_id": "test_message_broken", + "data": "test", + } + }, + ) + assert "status" in result and result["status"] == "Rejected" + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.asyncio +async def test_boot_notification_call_error( + test_config, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_boot_notification_call_error #########") + + test_controller.start() + + @on(Action.BootNotification) + def on_boot_notification_error(**kwargs): + raise InternalError() + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_error) + ) + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + # charge_point_v16.route_map[Action.Authorize]['_skip_schema_validation'] = True + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + charge_box_serial_number="cp001", + charge_point_model="Yeti", + charge_point_vendor="Pionix", + firmware_version="0.1", + ), + validate_boot_notification, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_accepted) + ) + + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(1) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + # charge_point_v16.route_map[Action.Authorize]['_skip_schema_validation'] = True + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + charge_box_serial_number="cp001", + charge_point_model="Yeti", + charge_point_vendor="Pionix", + firmware_version="0.1", + ), + validate_boot_notification, + timeout=70, + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-ocpp.yaml") +) +@pytest.mark.asyncio +@pytest.mark.inject_csms_mock +async def test_start_transaction_call_error_or_timeout( + test_config, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_start_transaction_call_error_or_timeout #########") + + test_controller.start() + + central_system_v16.mock.on_start_transaction.side_effect = [ + NotImplementedError(), + NotImplementedError(), + NotImplementedError(), + NotImplementedError(), + call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=IdTagInfo(status=AuthorizationStatus.accepted) + ), + ] + + charge_point_v16 = await central_system_v16.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + test_controller.swipe("DEADBEEF") + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, charge_point_v16, "StartTransaction", {} + ) + + await asyncio.sleep(2) + + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "StopTransaction", {"transactionId": 1} + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/firmware_and_diagnostics_tests.py b/tests/ocpp_tests/test_sets/ocpp16/firmware_and_diagnostics_tests.py new file mode 100644 index 000000000..e72d62cb5 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/firmware_and_diagnostics_tests.py @@ -0,0 +1,344 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +from datetime import datetime, timedelta +import logging +import asyncio +import getpass + +from ocpp.v16 import call, call_result +from ocpp.v16.enums import * + +# fmt: off +from validations import (validate_get_log) +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest_test_utils import * +# fmt: on + + +@pytest.mark.asyncio +async def test_get_diagnostics_retries( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_get_diagnostics_retries #########") + + await asyncio.sleep(1) + + # FIXME: make sure this port does not exist? or username and password are wrong? + location = f"ftp://{getpass.getuser()}:12345@localhost:2121" + start_time = datetime.utcnow() + stop_time = start_time + timedelta(days=3) + retries = 2 + retry_interval = 2 + + test_utility.messages.clear() + await charge_point_v16.get_diagnostics_req( + location=location, + start_time=start_time.isoformat(), + stop_time=stop_time.isoformat(), + retries=retries, + retry_interval=retry_interval, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.upload_failed), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.upload_failed), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.upload_failed), + ) + + +@pytest.mark.asyncio +async def test_upload_security_log_retries( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_upload_security_log_retries #########") + + oldest_timestamp = datetime.utcnow() + latest_timestamp = oldest_timestamp + timedelta(days=3) + retries = 2 + retry_interval = 2 + + log = { + "remoteLocation": f"ftp://{getpass.getuser()}:12345@localhost:2121", + "oldestTimestamp": oldest_timestamp.isoformat(), + "latestTimestamp": latest_timestamp.isoformat(), + } + + test_utility.messages.clear() + await charge_point_v16.get_log_req( + log=log, + log_type=Log.security_log, + retries=retries, + retry_interval=retry_interval, + request_id=1, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLog", + call_result.GetLogPayload(LogStatus.accepted), + validate_get_log, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.uploading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.upload_failure, 1), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.uploading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.upload_failure, 1), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.uploading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.upload_failure, 1), + ) + + +@pytest.mark.asyncio +async def test_firwmare_update_retries( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + # not supported when implemented security extensions + logging.info("######### test_firwmare_update_retries #########") + + await asyncio.sleep(1) + + retrieve_date = datetime.utcnow() + location = f"ftp://{getpass.getuser()}:12345@localhost:2121/firmware_update.pnx" + retries = 2 + retry_interval = 2 + + await charge_point_v16.update_firmware_req( + location=location, + retrieve_date=retrieve_date.isoformat(), + retries=retries, + retry_interval=retry_interval, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.downloading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.download_failed), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.downloading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.download_failed), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.downloading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.download_failed), + ) + + +@pytest.mark.asyncio +async def test_signed_update_firmware_retries( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_signed_update_firmware_retries #########") + + await asyncio.sleep(1) + + await charge_point_v16.change_configuration_req(key="HeartbeatInterval", value="20") + + certificate = open(test_config.certificate_info.mf_root_ca).read() + + await charge_point_v16.install_certificate_req( + certificate_type=CertificateUse.manufacturer_root_certificate, + certificate=certificate, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "InstallCertificate", + call_result.InstallCertificatePayload(CertificateStatus.accepted), + ) + + location = f"ftp://{getpass.getuser()}:12345@localhost:2121/firmware_update.pnx" + retries = 2 + retry_interval = 2 + retrieve_date_time = datetime.utcnow() + mf_root_ca = open(test_config.certificate_info.mf_root_ca).read() + fw_signature = open(test_config.firmware_info.update_file_signature).read() + + firmware = { + "location": location, + "retrieveDateTime": retrieve_date_time.isoformat(), + "signingCertificate": mf_root_ca, + "signature": fw_signature, + } + + await charge_point_v16.signed_update_firmware_req( + request_id=1, retries=retries, retry_interval=retry_interval, firmware=firmware + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedUpdateFirmware", + call_result.SignedUpdateFirmwarePayload(UpdateFirmwareStatus.accepted), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.downloading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.download_failed, 1), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.downloading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.download_failed, 1), + ) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.downloading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.download_failed, 1), + ) + + # no SignedFirmwareStatusNotification.req should be sent anymore + test_utility.forbidden_actions.append("SignedFirmwareStatusNotification") + test_utility.messages.clear() + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/message_queue.py b/tests/ocpp_tests/test_sets/ocpp16/message_queue.py new file mode 100644 index 000000000..8abc947cd --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/message_queue.py @@ -0,0 +1,143 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from unittest.mock import call as mock_call, ANY +import pytest +from everest.testing.core_utils.common import Requirement +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +from everest.testing.core_utils.probe_module import ProbeModule + +from ocpp.routing import create_route_map + +from ocpp.v16 import call +from ocpp.v16.enums import * + +# fmt: off +from validations import (validate_standard_start_transaction) +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest_test_utils import * +# fmt: on + + +@pytest.mark.asyncio +async def test_call_error_to_transaction_message( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + + setattr(charge_point_v16, "on_start_transaction", None) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="TransactionMessageAttempts", value="3" + ) + + test_controller.plug_in() + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + test_utility.messages.clear() + test_utility.forbidden_actions.append("StartTransaction") + + test_controller.plug_out() + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, charge_point_v16, "StopTransaction", {"reason": "EVDisconnected"} + ) + + +async def wait_for_mock_called(mock, call=None, timeout=2): + async def _await_called(): + while not mock.call_count or (call and call not in mock.mock_calls): + await asyncio.sleep(0.1) + + await asyncio.wait_for(_await_called(), timeout=timeout) + + +@pytest.mark.ocpp_version("ocpp1.6") +@pytest.mark.everest_core_config("everest-config-sil-ocpp.yaml") +@pytest.mark.inject_csms_mock +@pytest.mark.probe_module(connections={"ocpp": [Requirement("ocpp", "main")]}) +@pytest.mark.asyncio +async def test_security_event_delivery_after_reconnect( + everest_core, test_controller, central_system: CentralSystem +): + """Tests A04.FR.02 of OCPP 1.6 Security White Paper""" + + # Setup: Init Probe module, start EVerest and CSMS + test_controller.start() + csms_mock = central_system.mock + + probe_module = ProbeModule(everest_core.get_runtime_session()) + + probe_module.start() + await probe_module.wait_to_be_ready() + await central_system.wait_for_chargepoint() + + # Act: disconnect, send security event + test_controller.disconnect_websocket() + + csms_mock.on_security_event_notification.reset_mock() + # Since on boot we expect a count of security events + await probe_module.call_command( + "ocpp", "security_event", {"type": "SecurityLogWasCleared", "info": "test_info"} + ) + + # Verify: CSMS has not received any event (since offline), reconnect and verify event is received + await asyncio.sleep(1) + csms_mock.on_security_event_notification.assert_not_called() + + test_controller.connect_websocket() + + await wait_for_mock_called( + csms_mock.on_security_event_notification, + mock_call(tech_info="test_info", timestamp=ANY, type="SecurityLogWasCleared"), + 10, + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/ocpp_compliance_tests.py b/tests/ocpp_tests/test_sets/ocpp16/ocpp_compliance_tests.py new file mode 100755 index 000000000..5bb305bd6 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/ocpp_compliance_tests.py @@ -0,0 +1,6591 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import os +import pytest +from datetime import datetime, timedelta +import logging +import getpass +import asyncio + +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) + +# fmt: off + +from validations import ( + dont_validate_meter_values, + dont_validate_sign_certificate, + validate_composite_schedule, validate_get_log, + validate_meter_values, + validate_remote_start_stop_transaction, + validate_standard_start_transaction, + validate_standard_stop_transaction, + validate_boot_notification +) + +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode +from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP16ConfigAdjustment +from everest_test_utils import * +# fmt: on + +from ocpp.v16.enums import * +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result +from ocpp.routing import create_route_map, on +from ocpp.messages import unpack + + +@pytest.mark.asyncio +async def test_reset_to_idle( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_reset_to_idle #########") + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="0" + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + await charge_point_v16.change_configuration_req( + key="ClockAlignedDataInterval", value="0" + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="false" + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.change_availability_req( + connector_id=0, type=AvailabilityType.operative + ) + + await charge_point_v16.get_configuration_req(key=["AuthorizationCacheEnabled"]) + await charge_point_v16.get_configuration_req(key=["LocalAuthListEnabled"]) + + await charge_point_v16.send_local_list_req( + list_version=0, update_type=UpdateType.full + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + await charge_point_v16.get_configuration_req(key=["MaxChargingProfilesInstalled"]) + + await charge_point_v16.clear_charging_profile_req( + id=1, connector_id=1, charging_profile_purpose="TxDefaultProfile", stack_level=0 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ClearChargingProfile", + call_result.ClearChargingProfilePayload(ClearChargingProfileStatus.accepted), + ) + + +@pytest.mark.asyncio +async def test_stop_tx(test_controller: TestController, test_utility: TestUtility): + logging.info("######### test_stop_tx #########") + pass + + +@pytest.mark.asyncio +async def test_cold_boot( + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_cold_boot #########") + + test_controller.start() + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + await charge_point_v16.change_configuration_req(key="HeartbeatInterval", value="2") + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_cold_boot_pending( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_cold_boot_pending #########") + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=10, + status=RegistrationStatus.pending, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatus.accepted, + ) + + central_system_v16.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + response = await charge_point_v16.get_configuration_req() + assert len(response.configuration_key) > 20 + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + setattr(charge_point_v16, "on_boot_notification", on_boot_notification_accepted) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_regular_charging_session_plugin( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_regular_charging_session_plugin #########") + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_utility.validation_mode = ValidationMode.STRICT + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + test_utility.validation_mode = ValidationMode.EASY + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_regular_charging_session_identification( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_regular_charging_session_identification #########") + + await charge_point_v16.clear_cache_req() + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_utility.validation_mode = ValidationMode.STRICT + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + test_utility.validation_mode = ValidationMode.EASY + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # start charging session + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_regular_charging_session_identification_conn_timeout( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info( + "######### test_regular_charging_session_identification_conn_timeout #########" + ) + + await charge_point_v16.clear_cache_req() + await charge_point_v16.change_configuration_req(key="ConnectionTimeout", value="5") + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_utility.validation_mode = ValidationMode.STRICT + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StatusNotification with status available because of connection timeout + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_stop_transaction_match_tag( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_stop_transaction_match_tag #########") + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + # swipe wrong id tag + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload( + 0, "", 1, Reason.local, id_tag=test_config.authorization_info.valid_id_tag_1 + ), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_stop_transaction_parent_id_tag( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + logging.info("######### test_stop_transaction_parent_id_tag #########") + + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # swipe other id tag to authorize (same parent id) + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_005_1_ev_side_disconnect( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_005_1_ev_side_disconnect #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req( + key="StopTransactionOnEVSideDisconnect", value="true" + ) + await charge_point_v16.change_configuration_req( + key="UnlockConnectorOnEVSideDisconnect", value="true" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_controller.plug_out() + + test_utility.messages.clear() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.ev_disconnected), + validate_standard_stop_transaction, + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_ev_side_disconnect( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_ev_side_disconnect #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req( + key="StopTransactionOnEVSideDisconnect", value="true" + ) + await charge_point_v16.change_configuration_req( + key="UnlockConnectorOnEVSideDisconnect", value="true" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + test_controller.plug_out() + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.ev_disconnected), + validate_standard_stop_transaction, + ) + + await charge_point_v16.unlock_connector_req(connector_id=1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.unlocked), + ) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_ev_side_disconnect_tx_active( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_ev_side_disconnect_tx_active #########") + + await charge_point_v16.change_configuration_req( + key="MinimumStatusDuration", value="false" + ) + await charge_point_v16.clear_cache_req() + await charge_point_v16.change_configuration_req( + key="UnlockConnectorOnEVSideDisconnect", value="false" + ) + await charge_point_v16.change_configuration_req( + key="StopTransactionOnEVSideDisconnect", value="false" + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-two-connectors.yaml") +) +@pytest.mark.asyncio +async def test_one_reader_for_multiple_connectors( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_one_reader_for_multiple_connectors #########") + + await charge_point_v16.clear_cache_req() + + test_controller.swipe( + test_config.authorization_info.valid_id_tag_1, connectors=[1, 2] + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + test_controller.plug_in(connector_id=1) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + test_controller.swipe( + test_config.authorization_info.valid_id_tag_2, connectors=[1, 2] + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + test_controller.plug_in(connector_id=2) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 2, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 2, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + test_controller.swipe( + test_config.authorization_info.valid_id_tag_1, connectors=[1, 2] + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out(connector_id=1) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + test_controller.swipe( + test_config.authorization_info.valid_id_tag_2, connectors=[1, 2] + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 2, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 2, Reason.local), + validate_standard_stop_transaction, + ) + + test_controller.plug_out(connector_id=2) + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 2, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_regular_charge_session_cached_id( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_regular_charge_session_cached_id #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req( + key="AuthorizationCacheEnabled", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + await charge_point_v16.clear_cache_req() + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # charge for some time... + logging.debug("Charging for a while...") + await asyncio.sleep(2) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + logging.debug("executing unplug") + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + await asyncio.sleep(2) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # no authorize.req should be sent because id tag should be authorized locally + test_utility.forbidden_actions.append("Authorize") + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # charge for some time... + logging.debug("Charging for a while...") + await asyncio.sleep(2) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + +@pytest.mark.asyncio +async def test_clear_authorization_data_cache( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_clear_authorization_data_cache #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizationCacheEnabled", value="true" + ) + # expect ChangeConfiguration.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.change_configuration_req(key="ConnectionTimeout", value="2") + # expect ChangeConfiguration.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + # swipe valid id tag + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + test_utility.messages.clear() + + # expect StatusNotification with status available after connectionTimeout + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + await charge_point_v16.clear_cache_req() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ClearCache", + call_result.ClearCachePayload(ClearCacheStatus.accepted), + ) + + # swipe valid id tag + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + +@pytest.mark.asyncio +async def test_remote_charge_start_stop_cable_first( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_charge_start_stop_cable_first #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_remote_charge_start_stop_remote_first( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_charge_start_stop_remote_first #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # start charging session + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_remote_charge_start_timeout( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_charge_start_timeout #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.change_configuration_req(key="ConnectionTimeout", value="10") + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_remote_charge_stop( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_charge_stop #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + test_controller.plug_out() + + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_hard_reset_no_tx( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_hard_reset_no_tx #########") + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.inoperative + ) + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + await charge_point_v16.reset_req(type=ResetType.hard) + + test_controller.stop() + await asyncio.sleep(2) + + test_controller.start() + + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.operative + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_soft_reset_without_tx( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_soft_reset_without_tx #########") + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.inoperative + ) + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + await charge_point_v16.reset_req(type=ResetType.soft) + + test_controller.stop() + await asyncio.sleep(2) + + test_controller.start() + + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.operative + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_hard_reset_with_transaction( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_hard_reset_with_transaction #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.reset_req(type=ResetType.hard) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.hard_reset), + validate_standard_stop_transaction, + ) + + test_controller.stop() + + await asyncio.sleep(2) + + test_controller.start() + + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + +@pytest.mark.asyncio +async def test_soft_reset_with_transaction( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_soft_reset_with_transaction #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.reset_req(type=ResetType.soft) + + test_utility.validation_mode = ValidationMode.STRICT + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.soft_reset), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_unlock_connector_no_charging_no_fixed_cable( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_unlock_connector_no_charging #########") + + test_utility.validation_mode = ValidationMode.STRICT + await charge_point_v16.unlock_connector_req(connector_id=1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.unlocked), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL currently does not support this") +async def test_unlock_connector_no_charging_fixed_cable( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_unlock_connector_no_charging_fixed_cable #########") + + test_utility.validation_mode = ValidationMode.STRICT + await charge_point_v16.unlock_connector_req(connector_id=1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.not_supported), + timeout=10, + ) + + +@pytest.mark.asyncio +async def test_unlock_connector_with_charging_session_no_fixed_cable( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info( + "######### test_unlock_connector_with_charging_session_no_fixed_cable #########" + ) + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + await charge_point_v16.unlock_connector_req(connector_id=1) + + test_utility.validation_mode = ValidationMode.STRICT + await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.unlocked), + ) + + test_utility.validation_mode = ValidationMode.EASY + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.unlock_command), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL currently does not support this") +async def test_unlock_connector_with_charging_session_fixed_cable( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info( + "######### test_unlock_connector_with_charging_session_fixed_cable #########" + ) + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.unlock_connector_req(connector_id=1) + test_utility.validation_mode = ValidationMode.STRICT + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.not_supported), + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + test_utility.validation_mode = ValidationMode.EASY + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_get_configuration( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_get_configuration #########") + + test_utility.validation_mode = ValidationMode.STRICT + response = await charge_point_v16.get_configuration_req() + + assert len(response.configuration_key) > 20 + + +@pytest.mark.asyncio +async def test_set_configuration( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_set_configuration #########") + + test_utility.validation_mode = ValidationMode.STRICT + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="15" + ) + response = await charge_point_v16.get_configuration_req( + key=["MeterValueSampleInterval"] + ) + + assert response.configuration_key[0]["key"] == "MeterValueSampleInterval" + assert response.configuration_key[0]["value"] == "15" + + +@pytest.mark.asyncio +async def test_sampled_meter_values( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_sampled_meter_values #########") + + meter_value_sample_interval = "2" + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value=meter_value_sample_interval + ) + await charge_point_v16.change_configuration_req( + key="ClockAlignedDataInterval", value="0" + ) + + meter_values_sampled_data_response = await charge_point_v16.get_configuration_req( + key=["MeterValuesSampledData"] + ) + periodic_measurands = meter_values_sampled_data_response.configuration_key[0][ + "value" + ].split(",") + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [ + { + "key": "MeterValuesSampledData", + "readonly": False, + "value": "Energy.Active.Import.Register", + } + ] + ), + ) + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "AuthorizeRemoteTxRequests", "readonly": False, "value": "true"}] + ), + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + test_utility.validation_mode = ValidationMode.STRICT + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + test_utility.validation_mode = ValidationMode.EASY + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + meter_values_messages = [] + + logging.debug("Collecting meter values...") + while len(meter_values_messages) < 4: + raw_message = await asyncio.wait_for( + charge_point_v16.wait_for_message(), timeout=30 + ) + charge_point_v16.message_event.clear() + msg = unpack(raw_message) + if msg.action == "MeterValues": + meter_values_messages.append(msg) + logging.debug(f"Got {len(meter_values_messages)}...") + logging.debug("Collected meter values...") + assert validate_meter_values( + meter_values_messages, + periodic_measurands, + [], + int(meter_value_sample_interval), + 0, + ) + + test_utility.validation_mode = ValidationMode.EASY + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote, transaction_data=[]), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_clock_aligned_meter_values( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_clock_aligned_meter_values #########") + + await charge_point_v16.change_configuration_req( + key="ClockAlignedDataInterval", value="2" + ) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="2" + ) + + meter_values_sampled_data_response = await charge_point_v16.get_configuration_req( + key=["MeterValuesSampledData"] + ) + periodic_measurands = meter_values_sampled_data_response.configuration_key[0][ + "value" + ].split(",") + + meter_values_aligned_data_response = await charge_point_v16.get_configuration_req( + key=["MeterValuesAlignedData"] + ) + clock_aligned_measurands = meter_values_aligned_data_response.configuration_key[0][ + "value" + ].split(",") + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [ + { + "key": "MeterValuesSampledData", + "readonly": False, + "value": "Energy.Active.Import.Register", + } + ] + ), + ) + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "AuthorizeRemoteTxRequests", "readonly": False, "value": "true"}] + ), + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + test_utility.validation_mode = ValidationMode.STRICT + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + test_utility.validation_mode = ValidationMode.EASY + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + meter_values_messages = [] + + logging.debug("Collecting meter values...") + while len(meter_values_messages) < 6: + raw_message = await asyncio.wait_for( + charge_point_v16.wait_for_message(), timeout=30 + ) + charge_point_v16.message_event.clear() + msg = unpack(raw_message) + if msg.action == "MeterValues": + meter_values_messages.append(msg) + logging.debug(f"Got {len(meter_values_messages)}...") + logging.debug("Collected meter values...") + assert validate_meter_values( + meter_values_messages, periodic_measurands, clock_aligned_measurands, 2, 0 + ) + + test_utility.validation_mode = ValidationMode.EASY + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote, transaction_data=[]), + validate_standard_stop_transaction, + ) + + # unplug to finish simulation + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_authorize_invalid_blocked_expired( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_authorize_invalid_blocked_expired #########") + + await charge_point_v16.change_configuration_req( + key="MinimumStatusDuration", value="0" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_utility.validation_mode = ValidationMode.STRICT + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_1), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL cant simulate connector lock failure") +async def test_start_charging_session_lock_failure( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_start_charging_session_lock_failure #########") + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # TODO(piet): Simulate connector lock failure... + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.connector_lock_failure, ChargePointStatus.faulted + ), + ) + + +@pytest.mark.asyncio +async def test_remote_start_charging_session_rejected( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_start_charging_session_rejected #########") + + await charge_point_v16.change_configuration_req( + key="MinimumStatusDuration", value="0" + ) + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="false" + ) + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + test_utility.validation_mode = ValidationMode.STRICT + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + test_utility.validation_mode = ValidationMode.EASY + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.rejected), + validate_remote_start_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_remote_start_tx_connector_id_zero( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_remote_start_connector_id_zero #########") + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=0 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.rejected), + validate_remote_start_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_remote_stop_tx_rejected( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_stop_rejected #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + await asyncio.sleep(2) + + charge_point_v16.pipeline = [] + test_utility.validation_mode = ValidationMode.STRICT + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + test_utility.validation_mode = ValidationMode.EASY + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # send RemoteStopTransaction.req with invalid transaction_id + await charge_point_v16.remote_stop_transaction_req(transaction_id=3) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.rejected), + validate_remote_start_stop_transaction, + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL cant simulate connector lock failure") +async def test_unlock_connector_failure( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_unlock_connector_failure #########") + # TODO(piet): Put chargepoint in a state where unlock connector fails + await charge_point_v16.unlock_connector_req(connector_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.unlock_failed), + ) + + +@pytest.mark.asyncio +async def test_unlock_unknown_connector( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_unlock_unknown_connector #########") + + # send UnlockConnector.req with invalid connector id + await charge_point_v16.unlock_connector_req(connector_id=2) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "UnlockConnector", + call_result.UnlockConnectorPayload(UnlockStatus.not_supported), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL cant simulate powerloss with USV") +async def test_power_failure_going_down_charge_point_stop_tx( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info( + "######### test_power_failure_going_down_charge_point_stop_tx #########" + ) + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # TOOD(piet): Simulate power loss with USV + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.power_loss, transaction_data=[]), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL cant simulate powerloss with USV") +async def test_power_failure_boot_charge_point_stop_tx( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_power_failure_boot_charge_point_stop_tx #########") + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_controller.stop() + + await asyncio.sleep(2) + + test_controller.start() + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.power_loss, transaction_data=[]), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_power_failure_with_unavailable_status( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_power_failure_with_unavailable_status #########") + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.inoperative + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeAvailability", + call_result.ChangeAvailabilityPayload(AvailabilityStatus.accepted), + ) + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + test_controller.stop() + + await asyncio.sleep(2) + + test_controller.start() + + charge_point_v16 = await central_system_v16.wait_for_chargepoint() + charge_point_v16.pipe = True + + await asyncio.sleep(2) + + # expect StatusNotification with status unavailable + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.unavailable + ), + ) + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.operative + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_idle_charge_point( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_idle_charge_point #########") + + await charge_point_v16.change_configuration_req(key="HeartbeatInterval", value="10") + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(1) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + +@pytest.mark.asyncio +async def test_connection_loss_during_transaction( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_connection_loss_during_transaction #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + meter_values_sampled_data_response = await charge_point_v16.get_configuration_req( + key=["MeterValuesSampledData"] + ) + periodic_measurands = meter_values_sampled_data_response.configuration_key[0][ + "value" + ].split(",") + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(60) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + meter_values_messages = [] + + logging.debug("Collecting meter values...") + while len(meter_values_messages) < 6: + raw_message = await asyncio.wait_for( + charge_point_v16.wait_for_message(), timeout=30 + ) + charge_point_v16.message_event.clear() + msg = unpack(raw_message) + if msg.action == "MeterValues": + meter_values_messages.append(msg) + logging.debug(f"Got {len(meter_values_messages)}...") + logging.debug("Collected meter values...") + assert validate_meter_values(meter_values_messages, periodic_measurands, [], 10, 0) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + +@pytest.mark.asyncio +async def test_offline_start_transaction( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_offline_start_transaction #########") + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + + await asyncio.sleep(1) + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(1) + + # start charging session + test_controller.plug_in() + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(2) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_offline_start_transaction_restore( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_offline_start_transaction_restore #########") + + # StartTransaction.conf with invalid id + @on(Action.StartTransaction) + def on_start_transaction(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.invalid, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=id_tag_info + ) + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="true" + ) + await charge_point_v16.change_configuration_req( + key="StopTransactionOnInvalidId", value="false" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.clear_cache_req() + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # start charging session + test_controller.plug_in() + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + await asyncio.sleep(5) + + central_system_v16.function_overrides.append( + ("on_start_transaction", on_start_transaction) + ) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.invalid_id_tag, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # expect StatusNotification with status suspended_evse + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.suspended_evse + ), + ) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + # unplug to finish simulation + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_offline_start_transaction_restore_flow( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_offline_start_transaction_restore_flow #########") + + # StartTransaction.conf with invalid id + @on(Action.StartTransaction) + def on_start_transaction(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.invalid, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.StartTransactionPayload( + transaction_id=1, id_tag_info=id_tag_info + ) + + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="true" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.change_configuration_req( + key="StopTransactionOnInvalidId", value="true" + ) + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # start charging session + test_controller.plug_in() + + # swipe id tag to authorize + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + await asyncio.sleep(10) + + central_system_v16.function_overrides.append( + ("on_start_transaction", on_start_transaction) + ) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.invalid_id_tag, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.de_authorized), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + # unplug to finish simulation + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_offline_stop_transaction( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_offline_stop_transaction #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="10" + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(3) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # unplug to finish simulation + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_offline_transaction( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_offline_transaction #########") + + await charge_point_v16.change_configuration_req( + key="LocalAuthorizeOffline", value="true" + ) + await charge_point_v16.change_configuration_req( + key="AllowOfflineTxForUnknownId", value="true" + ) + + await asyncio.sleep(2) + + logging.debug("disconnect the ws connection...") + test_controller.disconnect_websocket() + + # start charging session + test_controller.plug_in() + + # swipe id tag to start transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # charge for some time... + logging.debug("Charging for a while...") + await asyncio.sleep(45) + + # swipe id tag to finish transaction + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + await asyncio.sleep(2) + + logging.debug("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.local), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + # unplug to finish simulation + test_controller.plug_out() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + +@pytest.mark.asyncio +async def test_configuration_keys( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_configuration_keys #########") + + await charge_point_v16.change_configuration_req(key="NotSupportedKey", value="true") + + # expect ChangeConfiguration.conf with status NotSupported + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.not_supported), + ) + + +@pytest.mark.asyncio +async def test_configuration_keys_incorrect( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_configuration_keys_incorrect #########") + + await charge_point_v16.change_configuration_req( + key="MeterValueSampleInterval", value="-1" + ) + + # expect ChangeConfiguration.conf with status NotSupported + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.rejected), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL currently does not support faulted state") +async def test_fault_behavior( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_fault_behavior #########") + + # Set Diode fault + test_controller.diode_fail() + # expect StatusNotification with status faulted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.ground_failure, ChargePointStatus.faulted + ), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-042_1.yaml") +) +@pytest.mark.asyncio +async def test_get_local_list_version( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_get_local_list_version #########") + + await charge_point_v16.change_configuration_req( + key="LocalAuthListEnabled", value="false" + ) + + # expect ChangeConfiguration.conf with status NotSupported + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.not_supported), + ) + + await charge_point_v16.get_local_list_version_req() + + # expect GetLocalListVersion.conf with status listVersion -1 + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLocalListVersion", + call_result.GetLocalListVersionPayload(list_version=-1), + ) + + +@pytest.mark.asyncio +async def test_get_local_list_version_flow( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_get_local_list_version_flow #########") + + await charge_point_v16.change_configuration_req( + key="LocalAuthListEnabled", value="true" + ) + + # expect ChangeConfiguration.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.send_local_list_req( + list_version=1, update_type=UpdateType.full + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + await charge_point_v16.get_local_list_version_req() + + # expect GetLocalListVersion.conf with status listVersion 0 + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLocalListVersion", + call_result.GetLocalListVersionPayload(list_version=0), + ) + + +@pytest.mark.asyncio +async def test_send_local_list( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_send_local_list #########") + + await charge_point_v16.change_configuration_req( + key="LocalAuthListEnabled", value="true" + ) + + # expect ChangeConfiguration.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.send_local_list_req( + list_version=1, + update_type=UpdateType.differential, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + await charge_point_v16.send_local_list_req( + list_version=1, + update_type=UpdateType.full, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-042_1.yaml") +) +@pytest.mark.asyncio +async def test_send_local_list_not_supported( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_send_local_list_not_supported #########") + + await charge_point_v16.send_local_list_req( + list_version=1, + update_type=UpdateType.full, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.not_supported), + ) + + +@pytest.mark.asyncio +async def test_send_local_list_ver_mismatch( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_send_local_list_ver_mismatch #########") + + await charge_point_v16.change_configuration_req( + key="LocalAuthListEnabled", value="true" + ) + + # expect ChangeConfiguration.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.send_local_list_req( + list_version=2, + update_type=UpdateType.full, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + await charge_point_v16.get_local_list_version_req() + + # expect GetLocalListVersion.conf with status listVersion 2 + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLocalListVersion", + call_result.GetLocalListVersionPayload(list_version=2), + ) + + await charge_point_v16.send_local_list_req( + list_version=5, + update_type=UpdateType.full, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.accepted), + ) + + await charge_point_v16.get_local_list_version_req() + + # expect GetLocalListVersion.conf with status listVersion 5 + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLocalListVersion", + call_result.GetLocalListVersionPayload(list_version=5), + ) + + await charge_point_v16.send_local_list_req( + list_version=4, + update_type=UpdateType.differential, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.version_mismatch), + ) + + await charge_point_v16.get_local_list_version_req() + + # expect GetLocalListVersion.conf with status listVersion -1 + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLocalListVersion", + call_result.GetLocalListVersionPayload(list_version=5), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip( + reason="EVerest SIL cannot put CP in a state where Send Local List fails" +) +async def test_send_local_list_failed( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_send_local_list_failed #########") + + # TODO(piet): Put cp in state where this fails + + await charge_point_v16.send_local_list_req( + list_version=0, update_type=UpdateType.full + ) + + # expect SendLocallist.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SendLocalList", + call_result.SendLocalListPayload(UpdateStatus.failed), + ) + + +@pytest.mark.asyncio +async def test_start_charging_id_in_authorization_list( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_start_charging_id_in_authorization_list #########") + + await charge_point_v16.change_configuration_req( + key="LocalPreAuthorize", value="true" + ) + await charge_point_v16.change_configuration_req( + key="AuthorizationCacheEnabled", value="false" + ) + await charge_point_v16.change_configuration_req( + key="LocalAuthListEnabled", value="true" + ) + + response = await charge_point_v16.get_local_list_version_req() + await charge_point_v16.send_local_list_req( + list_version=response.list_version, + update_type=UpdateType.full, + local_authorization_list=[ + { + "idTag": "RFID1", + "idTagInfo": { + "status": "Accepted", + "expiryDate": "2342-06-19T09:10:00.000Z", + "parentIdTag": "PTAG", + }, + } + ], + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.xdist_group(name="FTP") +async def test_firwmare_update_donwload_install( + charge_point_v16: ChargePoint16, test_utility: TestUtility, ftp_server, test_config +): + # not supported when implemented security extensions + logging.info("######### test_firwmare_update_donwload_install #########") + + retrieve_date = datetime.utcnow() + location = f"ftp://{getpass.getuser()}:12345@localhost:{ftp_server.port}/firmware_update.pnx" + + await charge_point_v16.update_firmware_req( + location=location, retrieve_date=retrieve_date.isoformat() + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.downloading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.downloaded), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.installing), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "FirmwareStatusNotification", + call.DiagnosticsStatusNotificationPayload(FirmwareStatus.installed), + ) + + +@pytest.mark.asyncio +async def test_firwmare_update_download_fail( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + # not supported when implemented security extensions + logging.info("######### test_firwmare_update_download_fail #########") + pass + + +@pytest.mark.asyncio +async def test_firwmare_update_install_fail( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + # not supported when implemented security extensions + logging.info("######### test_firwmare_update_install_fail #########") + pass + + +@pytest.mark.asyncio +@pytest.mark.xdist_group(name="FTP") +async def test_get_diagnostics( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, + ftp_server, +): + logging.info("######### test_get_diagnostics #########") + + await asyncio.sleep(1) + + location = f"ftp://{getpass.getuser()}:12345@localhost:{ftp_server.port}" + start_time = datetime.utcnow() + stop_time = start_time + timedelta(days=3) + + await charge_point_v16.get_diagnostics_req( + location=location, + start_time=start_time.isoformat(), + stop_time=stop_time.isoformat(), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploaded), + ) + + +@pytest.mark.asyncio +async def test_get_diagnostics_upload_fail( + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_get_diagnostics_upload_fail #########") + + location = "ftp://pionix:12345@notavalidftpserver:21" + start_time = datetime.utcnow() + stop_time = start_time + timedelta(days=3) + retries = 0 + + await charge_point_v16.get_diagnostics_req( + location=location, + start_time=start_time.isoformat(), + stop_time=stop_time.isoformat(), + retries=retries, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.uploading), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.upload_failed), + ) + + +@pytest.mark.asyncio +async def test_reservation_local_start_tx( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_local_start_tx #########") + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date="2030-06-19T09:10:00.000Z", + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + # swipe invalid id tag + test_controller.swipe(test_config.authorization_info.invalid_id_tag) + + # swipe valid id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # start charging session + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_reservation_remote_start_tx( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_remote_start_tx #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date="2030-06-19T09:10:00.000Z", + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # start charging session + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_reservation_connector_expire( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_connector_expire #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + t = datetime.utcnow() + timedelta(seconds=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # start charging session + test_controller.plug_in() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_reservation_connector_faulted( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_connector_faulted #########") + + # Set diode fault + test_controller.diode_fail() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + + await asyncio.sleep(10) + + t = datetime.utcnow() + timedelta(seconds=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.faulted), + ) + + +@pytest.mark.asyncio +async def test_reservation_connector_occupied( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_connector_occupied #########") + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + t = datetime.utcnow() + timedelta(seconds=10) + + await asyncio.sleep(2) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.occupied), + ) + + +@pytest.mark.asyncio +async def test_reservation_connector_unavailable( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_reservation_connector_unavailable #########") + + await charge_point_v16.change_availability_req( + connector_id=1, type=AvailabilityType.inoperative + ) + + t = datetime.utcnow() + timedelta(seconds=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.unavailable), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-042_1.yaml") +) +@pytest.mark.asyncio +async def test_reservation_connector_rejected( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_reservation_connector_rejected #########") + + t = datetime.utcnow() + timedelta(seconds=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status rejected + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.rejected), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip( + reason="Libocpp currently doesnt support ReserveConnectorZeroSupported" +) +async def test_reservation_transaction( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_reservation_transaction #########") + + # FIXME: implement this missing testcase! + + await charge_point_v16.change_configuration_req( + key="ReserveConnectorZeroSupported", value="true" + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="EVerest SIL currently does not support faulted state") +async def test_reservation_faulted_state( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_reservation_faulted_state #########") + + test_controller.diode_fail() + + # expect StatusNotification with status faulted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.ground_failure, ChargePointStatus.faulted + ), + ) + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=datetime.utcnow().isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status rejected + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.faulted), + ) + + +@pytest.mark.asyncio +async def test_reservation_occupied_state( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_reservation_occupied_state #########") + + test_controller.plug_in() + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=(datetime.utcnow() + timedelta(minutes=10)).isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status rejected + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.occupied), + ) + + +@pytest.mark.asyncio +async def test_reservation_cancel( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_reservation_cancel #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + await charge_point_v16.cancel_reservation_req(reservation_id=0) + + # expect CancelReservation.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "CancelReservation", + call_result.CancelReservationPayload(CancelReservationStatus.accepted), + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_reservation_cancel_rejected( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_reservation_cancel_rejected #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + await charge_point_v16.cancel_reservation_req(reservation_id=2) + + # expect CancelReservation.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "CancelReservation", + call_result.CancelReservationPayload(CancelReservationStatus.rejected), + ) + + +@pytest.mark.asyncio +async def test_reservation_with_partentid( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_reservation_with_partentid #########") + + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTagInfo( + status=AuthorizationStatus.accepted, + parent_id_tag=test_config.authorization_info.parent_id_tag, + ) + return call_result.AuthorizePayload(id_tag_info=id_tag_info) + + setattr(charge_point_v16, "on_authorize", on_authorize) + charge_point_v16.route_map = create_route_map(charge_point_v16) + + await charge_point_v16.change_configuration_req( + key="AuthorizeRemoteTxRequests", value="true" + ) + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v16.reserve_now_req( + connector_id=1, + expiry_date=t.isoformat(), + id_tag=test_config.authorization_info.valid_id_tag_1, + parent_id_tag=test_config.authorization_info.parent_id_tag, + reservation_id=0, + ) + + # expect ReserveNow.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ReserveNow", + call_result.ReserveNowPayload(ReservationStatus.accepted), + ) + + # expect StatusNotification.req with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_2, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "Authorize", + call.AuthorizePayload(test_config.authorization_info.valid_id_tag_2), + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await charge_point_v16.remote_stop_transaction_req(transaction_id=1) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStopTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StopTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StopTransaction", + call.StopTransactionPayload(0, "", 1, Reason.remote), + validate_standard_stop_transaction, + ) + + # expect StatusNotification with status finishing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.finishing + ), + ) + + +@pytest.mark.asyncio +async def test_trigger_message( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_trigger_message #########") + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.meter_values, connector_id=1 + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "MeterValues", + call.MeterValuesPayload(0, []), + dont_validate_meter_values, + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.heartbeat + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + assert await wait_for_and_validate( + test_utility, charge_point_v16, "Heartbeat", call.HeartbeatPayload() + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.status_notification + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + # expect StatusNotification.req with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.diagnostics_status_notification + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + # expect DiagnosticsStatusNotificationPayload.req with status idle + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DiagnosticsStatusNotification", + call.DiagnosticsStatusNotificationPayload(DiagnosticsStatus.idle), + ) + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.boot_notification + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.accepted), + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "BootNotification", + call.BootNotificationPayload( + test_config.charge_point_info.charge_point_model, + charge_box_serial_number=test_config.charge_point_info.charge_point_id, + charge_point_vendor=test_config.charge_point_info.charge_point_vendor, + firmware_version=test_config.charge_point_info.firmware_version, + ), + validate_boot_notification, + ) + + +@pytest.mark.asyncio +async def test_trigger_message_rejected( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_trigger_message_rejected #########") + + await charge_point_v16.trigger_message_req( + requested_message=MessageTrigger.meter_values, connector_id=2 + ) + # expect TriggerMessage.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "TriggerMessage", + call_result.TriggerMessagePayload(TriggerMessageStatus.rejected), + ) + + +@pytest.mark.asyncio +async def test_central_charging_tx_default_profile( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_central_charging_tx_default_profile #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + cs = await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=300) + + passed_seconds = int((datetime.utcnow() - valid_from).total_seconds()) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + connector_id=1, + schedule_start=valid_from.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60 - passed_seconds, limit=10), + ChargingSchedulePeriod(start_period=120 - passed_seconds, limit=8), + ChargingSchedulePeriod(start_period=300 - passed_seconds, limit=48), + ], + ), + ) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_central_charging_tx_profile( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_central_charging_tx_profile #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=300) + + passed_seconds = int((datetime.utcnow() - valid_from).total_seconds()) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=valid_from.isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60 - passed_seconds, limit=10), + ChargingSchedulePeriod(start_period=120 - passed_seconds, limit=8), + ChargingSchedulePeriod(start_period=260 - passed_seconds, limit=48), + ], + ), + ) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_central_charging_no_transaction( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_central_charging_no_transaction #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.rejected), + ) + + +@pytest.mark.asyncio +async def test_central_charging_wrong_tx_id( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_central_charging_wrong_tx_id #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=3, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.rejected), + ) + + +@pytest.mark.asyncio +async def test_central_charging_tx_default_profile_ongoing_transaction( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info( + "######### test_central_charging_tx_default_profile_ongoing_transaction #########" + ) + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=300) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + connector_id=1, + schedule_start=valid_from.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_get_composite_schedule( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_get_composite_schedule #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req_1 = call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=86400, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10) + ], + ), + ), + ) + + set_charging_profile_req_2 = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ChargingSchedulePeriod(start_period=180, limit=25), + ChargingSchedulePeriod(start_period=260, limit=8), + ], + ), + ), + ) + + set_charging_profile_req_3 = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=8), + ChargingSchedulePeriod(start_period=50, limit=11), + ChargingSchedulePeriod(start_period=140, limit=16), + ChargingSchedulePeriod(start_period=200, limit=6), + ChargingSchedulePeriod(start_period=240, limit=12), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req_1) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + # await asyncio.sleep(2) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req_2) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + # await asyncio.sleep(2) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req_3) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + cs = await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=400) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + connector_id=1, + schedule_start=valid_from.isoformat(), + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=8), + ChargingSchedulePeriod(start_period=50, limit=10), + ChargingSchedulePeriod(start_period=200, limit=6), + ChargingSchedulePeriod(start_period=240, limit=10), + ChargingSchedulePeriod(start_period=260, limit=8), + ChargingSchedulePeriod(start_period=300, limit=10), + ], + ), + ) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_clear_charging_profile( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_clear_charging_profile #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + await charge_point_v16.clear_charging_profile_req( + id=1, connector_id=1, charging_profile_purpose="TxDefaultProfile", stack_level=0 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ClearChargingProfile", + call_result.ClearChargingProfilePayload(ClearChargingProfileStatus.accepted), + ) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status="Accepted", + connector_id=1, + charging_schedule=ChargingSchedule( + charging_schedule_period=[], + duration=400, + charging_rate_unit=ChargingRateUnitType.amps, + ), + ) + + await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=400) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_stacking_charging_profiles( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_stacking_charging_profiles #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + await charge_point_v16.get_configuration_req(key=["MaxChargingProfilesInstalled"]) + await charge_point_v16.get_configuration_req(key=["ChargeProfileMaxStackLevel"]) + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + set_charging_profile_req_1 = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=50, limit=5), + ChargingSchedulePeriod(start_period=100, limit=8), + ChargingSchedulePeriod(start_period=200, limit=10), + ], + ), + ), + ) + + set_charging_profile_req_2 = call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=150, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=7), + ChargingSchedulePeriod(start_period=100, limit=9), + ], + ), + ), + ) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req_1) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + await asyncio.sleep(2) + + await charge_point_v16.set_charging_profile_req(set_charging_profile_req_2) + # expect SetChargingProfile.conf with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SetChargingProfile", + call_result.SetChargingProfilePayload(ChargingProfileStatus.accepted), + ) + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + cs = await charge_point_v16.get_composite_schedule_req(connector_id=1, duration=350) + + passed_seconds = int((datetime.utcnow() - valid_from).total_seconds()) + + exp_get_composite_schedule_response = call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + connector_id=1, + schedule_start=valid_from.isoformat(), + charging_schedule=ChargingSchedule( + duration=350, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=7), + ChargingSchedulePeriod(start_period=100 - passed_seconds, limit=9), + ChargingSchedulePeriod(start_period=150 - passed_seconds, limit=8), + ChargingSchedulePeriod(start_period=200 - passed_seconds, limit=10), + ], + ), + ) + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_get_composite_schedule_response, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_remote_start_tx_with_profile( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + logging.info("######### test_remote_start_tx_with_profile #########") + + await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"]) + + # start charging session + test_controller.plug_in() + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + cs_charging_profiles = ChargingProfile( + charging_profile_id=1, + stack_level=2, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=30, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ChargingSchedulePeriod(start_period=0, limit=6)], + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, + connector_id=1, + charging_profile=cs_charging_profiles, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + +@pytest.mark.asyncio +async def test_remote_start_tx_with_profile_rejected( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_remote_start_tx_with_profile_rejected #########") + + valid_from = datetime.utcnow() + valid_to = valid_from + timedelta(days=3) + + cs_charging_profiles = ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=valid_from.isoformat(), + valid_to=valid_to.isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=valid_from.isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ) + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, + connector_id=1, + charging_profile=cs_charging_profiles, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.rejected), + validate_remote_start_stop_transaction, + ) + + +@pytest.mark.asyncio +async def test_data_transfer_to_chargepoint( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + logging.info("######### test_data_transfer_to_chargepoint #########") + + await charge_point_v16.data_transfer_req( + vendor_id="VENID", message_id="MSGID", data="Data1" + ) + + success = await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), + timeout=5, + ) + + if not success: + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.unknown_vendor_id), + timeout=5, + ) + else: + assert success + + +@pytest.mark.asyncio +async def test_data_transfer_to_csms( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + # TODO(piet): Need to trigger DataTransfer.req from cp->cs for that via mqtt + # (somehow similiar to websocket control) + logging.info("######### test_data_transfer_to_csms #########") + pass + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-1.yaml") +) +@pytest.mark.asyncio +async def test_chargepoint_update_http_auth_key( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_chargepoint_update_http_auth_key #########") + + await charge_point_v16.change_configuration_req( + key="AuthorizationKey", value="4f43415f4f4354545f61646d696e5f74657374" + ) + # expect ChangeConfiguration.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + # wait for reconnect + await central_system_v16.wait_for_chargepoint(wait_for_bootnotification=False) + + charge_point_v16 = central_system_v16.chargepoint + test_utility = TestUtility() + + response = await charge_point_v16.get_configuration_req() + assert len(response.configuration_key) > 20 + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-2.yaml") +) +@pytest.mark.source_certs_dir(Path(__file__).parent / "../everest-aux/certs") +@pytest.mark.asyncio +@pytest.mark.csms_tls +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Internal", "VerifyCsmsCommonName", False)]) +) +async def test_chargepoint_update_certificate( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_chargepoint_update_certificate #########") + + await charge_point_v16.change_configuration_req(key="CpoName", value="VENID") + # expect ChangeConfiguration.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.sign_charge_point_certificate + ) + # expect ExtendedTriggerMessage.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ExtendedTriggerMessage", + call_result.ExtendedTriggerMessagePayload(TriggerMessageStatus.accepted), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignCertificate", + call.SignCertificatePayload(csr=""), + dont_validate_sign_certificate, + ) + + await charge_point_v16.certificate_signed_req( + csms_root_ca=test_config.certificate_info.csms_root_ca, + csms_root_ca_key=test_config.certificate_info.csms_root_ca_key, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "CertificateSigned", + call_result.CertificateSignedPayload(CertificateSignedStatus.accepted), + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-security-profile-2.yaml") +) +@pytest.mark.asyncio +@pytest.mark.source_certs_dir(Path(__file__).parent / "../everest-aux/certs") +@pytest.mark.csms_tls +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Internal", "VerifyCsmsCommonName", False)]) +) +async def test_chargepoint_install_certificate( + test_config: OcppTestConfiguration, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_chargepoint_install_certificate #########") + + certificate = open( + Path(__file__).parent.parent / test_config.certificate_info.csms_cert + ).read() + + await charge_point_v16.install_certificate_req( + certificate_type=CertificateUse.central_system_root_certificate, + certificate=certificate, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "InstallCertificate", + call_result.InstallCertificatePayload(CertificateStatus.accepted), + ) + + r = await charge_point_v16.get_installed_certificate_ids_req( + certificate_type=CertificateUse.central_system_root_certificate + ) + + exp_cert_hash_data = { + "hash_algorithm": "SHA256", + "issuer_key_hash": "7569e411948ceda3d815f04caab0d8548035c624116e1be688344ef095aea53b", + "issuer_name_hash": "d3768132ad54b8162680d1ba88966d189e5719d226524f6cd893c5f1c506f068", + "serial_number": "59d2755839892c132eaa9a49ad6c71638ce8012b", + } + assert exp_cert_hash_data in r.certificate_hash_data + + await charge_point_v16.delete_certificate_req( + certificate_hash_data=exp_cert_hash_data + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DeleteCertificate", + call_result.DeleteCertificatePayload(DeleteCertificateStatus.accepted), + ) + + +@pytest.mark.asyncio +async def test_chargepoint_delete_certificate( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_chargepoint_delete_certificate #########") + + r = await charge_point_v16.get_installed_certificate_ids_req( + certificate_type=CertificateUse.central_system_root_certificate + ) + + if r.status == GetInstalledCertificateStatus.not_found: + certificate = open(test_config.certificate_info.csms_root_ca).read() + await charge_point_v16.install_certificate_req( + certificate_type=CertificateUse.central_system_root_certificate, + certificate=certificate, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "InstallCertificate", + call_result.InstallCertificatePayload(CertificateStatus.accepted), + ) + + certificate_hash_data = { + "hashAlgorithm": "SHA256", + "issuerKeyHash": "89ea6977e786fcbaeb4f04e4ccdbfaa6a6088e8ba8f7404033ac1b3a62bc36a1", + "issuerNameHash": "e60bd843bf2279339127ca19ab6967081dd6f95e745dc8b8632fa56031debe5b", + "serialNumber": "1", + } + + test_utility.validation_mode = ValidationMode.STRICT + + await charge_point_v16.delete_certificate_req( + certificate_hash_data=certificate_hash_data + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DeleteCertificate", + call_result.DeleteCertificatePayload(DeleteCertificateStatus.accepted), + ) + + await charge_point_v16.delete_certificate_req( + certificate_hash_data=certificate_hash_data + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DeleteCertificate", + call_result.DeleteCertificatePayload(DeleteCertificateStatus.not_found), + ) + + +@pytest.mark.asyncio +async def test_chargepoint_invalid_certificate_security_event( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info( + "######### test_chargepoint_invalid_certificate_security_event #########" + ) + + await charge_point_v16.extended_trigger_message_req( + requested_message=MessageTrigger.sign_charge_point_certificate + ) + # expect ExtendedTriggerMessage.conf with status Accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ExtendedTriggerMessage", + call_result.ExtendedTriggerMessagePayload(TriggerMessageStatus.accepted), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignCertificate", + call.SignCertificatePayload(csr=""), + dont_validate_sign_certificate, + ) + + # this is an invalid certificate chain + await charge_point_v16.certificate_signed_req( + csms_root_ca=test_config.certificate_info.csms_root_ca, + csms_root_ca_key=test_config.certificate_info.csms_root_ca_key, + certificate_chain="-----BEGIN CERTIFICATE-----\nMIHgMIGaAgEBMA0GCSqG-----END CERTIFICATE-----", + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "CertificateSigned", + call_result.CertificateSignedPayload(CertificateSignedStatus.rejected), + ) + + # InvalidChargePointCertificate is defined as critical in OCPP1.6 + # assert await wait_for_and_validate(test_utility, charge_point_v16, "SecurityEventNotification", {"type": "InvalidChargePointCertificate"}) + + +@pytest.mark.asyncio +async def test_chargepoint_invalid_central_system_security_event( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + central_system_v16: CentralSystem, + test_utility: TestUtility, +): + logging.info( + "######### test_chargepoint_invalid_central_system_security_event #########" + ) + + await charge_point_v16.install_certificate_req( + certificate_type=CertificateUse.central_system_root_certificate, + certificate="-----BEGIN CERTIFICATE-----\nMIHgMIGaAgEBMA0GCSqG-----END CERTIFICATE-----", + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "InstallCertificate", + call_result.InstallCertificatePayload(CertificateStatus.rejected), + ) + # InvalidCentralSystemCertificate is defined as critical in OCPP1.6 + # assert await wait_for_and_validate(test_utility, charge_point_v16, "SecurityEventNotification", {"type": "InvalidCentralSystemCertificate"}) + + +@pytest.mark.asyncio +@pytest.mark.xdist_group(name="FTP") +async def test_get_security_log( + charge_point_v16: ChargePoint16, test_utility: TestUtility, ftp_server +): + logging.info("######### test_get_security_log #########") + + oldest_timestamp = datetime.utcnow() + latest_timestamp = oldest_timestamp + timedelta(days=3) + + log = { + "remoteLocation": f"ftp://{getpass.getuser()}:12345@localhost:{ftp_server.port}", + "oldestTimestamp": oldest_timestamp.isoformat(), + "latestTimestamp": latest_timestamp.isoformat(), + } + + await charge_point_v16.get_log_req(log=log, log_type=Log.security_log, request_id=1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetLog", + call_result.GetLogPayload(LogStatus.accepted), + validate_get_log, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.uploading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "LogStatusNotification", + call.LogStatusNotificationPayload(UploadLogStatus.uploaded, 1), + ) + + +@pytest.mark.asyncio +@pytest.mark.xdist_group(name="FTP") +async def test_signed_update_firmware( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + ftp_server, +): + logging.info("######### test_signed_update_firmware #########") + + certificate = open(test_config.certificate_info.mf_root_ca).read() + + await charge_point_v16.install_certificate_req( + certificate_type=CertificateUse.manufacturer_root_certificate, + certificate=certificate, + ) + + os.system( + f"curl -T {Path(__file__).parent.parent / test_config.firmware_info.update_file} ftp://{getpass.getuser()}:12345@localhost:{ftp_server.port}" + ) + + location = f"ftp://{getpass.getuser()}:12345@localhost:{ftp_server.port}/firmware_update.pnx" + retrieve_date_time = datetime.utcnow() + mf_root_ca = open(test_config.certificate_info.mf_root_ca).read() + fw_signature = open(test_config.firmware_info.update_file_signature).read() + + firmware = { + "location": location, + "retrieveDateTime": retrieve_date_time.isoformat(), + "signingCertificate": mf_root_ca, + "signature": fw_signature, + } + + await charge_point_v16.signed_update_firmware_req(request_id=1, firmware=firmware) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedUpdateFirmware", + call_result.SignedUpdateFirmwarePayload(UpdateFirmwareStatus.accepted), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.downloading, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.downloaded, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload( + FirmwareStatus.signature_verified, 1 + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.installing, 1), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "SignedFirmwareStatusNotification", + call.SignedFirmwareStatusNotificationPayload(FirmwareStatus.installed, 1), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py b/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py new file mode 100644 index 000000000..5307cd4c9 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py @@ -0,0 +1,731 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import asyncio +from dataclasses import dataclass +from unittest.mock import Mock, call as mock_call, ANY +import logging + +import pytest +import pytest_asyncio +from everest.testing.core_utils.common import Requirement +from everest.testing.core_utils.everest_core import EverestCore +from everest.testing.core_utils.probe_module import ProbeModule +from everest.testing.ocpp_utils.central_system import CentralSystem +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from ocpp.v16.call import SetChargingProfilePayload + +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP16ConfigAdjustment, +) + + +@dataclass +class _OCPP16GenericInterfaceIntegrationEnvironment: + csms_mock: Mock + central_system: CentralSystem + everest_core: EverestCore + probe_module: ProbeModule + probe_module_command_mocks: dict[str, dict[str, Mock]] + charge_point: ChargePoint16 + + +@pytest_asyncio.fixture +async def _env( + everest_core, + test_controller, + central_system: CentralSystem, + skip_implementation, + overwrite_implementation, +): + test_controller.start() + csms_mock = central_system.mock + + probe_module = ProbeModule(everest_core.get_runtime_session()) + probe_module_command_mocks = {} + + def _add_pm_command_mock(implementation_id, command, value, skip_implementation): + skip = False + if skip_implementation: + if implementation_id in skip_implementation: + to_skip = skip_implementation[implementation_id] + if command in to_skip: + logging.info(f"Skipping implementation of {command}") + skip = True + if not skip: + if overwrite_implementation: + logging.info(f"OVERW: {overwrite_implementation}") + if implementation_id in overwrite_implementation: + to_overwrite = overwrite_implementation[implementation_id] + if command in to_overwrite: + logging.info(f"Overwriting implementation of {command}") + value = to_overwrite[command] + probe_module_command_mocks.setdefault(implementation_id, {})[ + command + ] = Mock() + probe_module_command_mocks[implementation_id][command].return_value = value + probe_module.implement_command( + implementation_id=implementation_id, + command_name=command, + handler=probe_module_command_mocks[implementation_id][command], + ) + + for idx, evse_manager in enumerate(["evse_manager", "evse_manager_b"]): + _add_pm_command_mock( + evse_manager, + "get_evse", + {"id": idx + 1, "connectors": [{"id": 1}]}, + skip_implementation, + ) + _add_pm_command_mock(evse_manager, "enable_disable", True, skip_implementation) + _add_pm_command_mock( + evse_manager, "authorize_response", None, skip_implementation + ) + _add_pm_command_mock( + evse_manager, "withdraw_authorization", None, skip_implementation + ) + _add_pm_command_mock(evse_manager, "reserve", False, skip_implementation) + _add_pm_command_mock( + evse_manager, "cancel_reservation", None, skip_implementation + ) + _add_pm_command_mock(evse_manager, "set_faulted", None, skip_implementation) + _add_pm_command_mock(evse_manager, "pause_charging", True, skip_implementation) + _add_pm_command_mock(evse_manager, "resume_charging", True, skip_implementation) + _add_pm_command_mock( + evse_manager, "stop_transaction", True, skip_implementation + ) + _add_pm_command_mock(evse_manager, "force_unlock", True, skip_implementation) + _add_pm_command_mock( + evse_manager, "set_get_certificate_response", None, skip_implementation + ) + _add_pm_command_mock( + evse_manager, "external_ready_to_start_charging", True, skip_implementation + ) + _add_pm_command_mock( + "security", "get_leaf_expiry_days_count", 42, skip_implementation + ) + _add_pm_command_mock( + "security", + "get_v2g_ocsp_request_data", + {"ocsp_request_data_list": []}, + skip_implementation, + ) + _add_pm_command_mock( + "security", + "get_mo_ocsp_request_data", + {"ocsp_request_data_list": []}, + skip_implementation, + ) + _add_pm_command_mock( + "security", "install_ca_certificate", "Accepted", skip_implementation + ) + _add_pm_command_mock( + "security", "delete_certificate", "Accepted", skip_implementation + ) + _add_pm_command_mock( + "security", "update_leaf_certificate", "Accepted", skip_implementation + ) + _add_pm_command_mock("security", "verify_certificate", "Valid", skip_implementation) + _add_pm_command_mock( + "security", + "get_installed_certificates", + {"status": "Accepted", "certificate_hash_data_chain": []}, + skip_implementation, + ) + _add_pm_command_mock("security", "update_ocsp_cache", None, skip_implementation) + _add_pm_command_mock( + "security", "is_ca_certificate_installed", False, skip_implementation + ) + _add_pm_command_mock( + "security", + "generate_certificate_signing_request", + {"status": "Accepted"}, + skip_implementation, + ) + _add_pm_command_mock( + "security", + "get_leaf_certificate_info", + {"status": "Accepted"}, + skip_implementation, + ) + _add_pm_command_mock("security", "get_verify_file", "", skip_implementation) + _add_pm_command_mock("security", "verify_file_signature", True, skip_implementation) + _add_pm_command_mock( + "security", + "get_all_valid_certificates_info", + {"status": "NotFound", "info": []}, + skip_implementation, + ) + _add_pm_command_mock("auth", "set_connection_timeout", None, skip_implementation) + _add_pm_command_mock("auth", "set_master_pass_group_id", None, skip_implementation) + _add_pm_command_mock( + "reservation", "cancel_reservation", "Accepted", skip_implementation + ) + _add_pm_command_mock("reservation", "reserve_now", False, skip_implementation) + _add_pm_command_mock("reservation", "exists_reservation", False, skip_implementation) + _add_pm_command_mock("system", "get_boot_reason", "PowerUp", skip_implementation) + _add_pm_command_mock("system", "update_firmware", "Accepted", skip_implementation) + _add_pm_command_mock( + "system", "allow_firmware_installation", None, skip_implementation + ) + _add_pm_command_mock("system", "upload_logs", "Accepted", skip_implementation) + _add_pm_command_mock("system", "is_reset_allowed", True, skip_implementation) + _add_pm_command_mock("system", "reset", None, skip_implementation) + _add_pm_command_mock("system", "set_system_time", True, skip_implementation) + + probe_module.start() + await probe_module.wait_to_be_ready() + for evse_manager in ["evse_manager", "evse_manager_b"]: + probe_module.publish_variable(evse_manager, "ready", True) + + await central_system.wait_for_chargepoint() + + yield _OCPP16GenericInterfaceIntegrationEnvironment( + csms_mock, + central_system, + everest_core, + probe_module, + probe_module_command_mocks, + central_system.chargepoint, + ) + test_controller.stop() + + +class CSMSConnectionUtils: + def __init__(self, central_system: CentralSystem): + self._central_system = central_system + + @property + def is_connected(self) -> bool: + if not self._central_system.ws_server.websockets: + return False + assert len(self._central_system.ws_server.websockets) == 1 + connection = next(iter(self._central_system.ws_server.websockets)) + return connection.open + + +async def wait_for_mock_called(mock, call=None, timeout=2): + async def _await_called(): + while not mock.call_count or (call and call not in mock.mock_calls): + await asyncio.sleep(0.1) + + await asyncio.wait_for(_await_called(), timeout=timeout) + + +@pytest.mark.ocpp_version("ocpp1.6") +@pytest.mark.everest_core_config("everest-config-ocpp16-probe-module.yaml") +@pytest.mark.inject_csms_mock +@pytest.mark.probe_module(connections={"ocpp": [Requirement("ocpp", "ocpp_generic")]}) +@pytest.mark.asyncio +class TestOCPP16GenericInterfaceIntegration: + + async def test_command_stop(self, _env): + csms_connection = CSMSConnectionUtils(_env.central_system) + assert csms_connection.is_connected + res = await _env.probe_module.call_command("ocpp", "stop", None) + assert res is True + await asyncio.sleep(5) + assert not csms_connection.is_connected + + async def test_command_restart(self, _env): + csms_connection = CSMSConnectionUtils(_env.central_system) + await _env.probe_module.call_command("ocpp", "stop", None) + await asyncio.sleep(5) + assert not csms_connection.is_connected + res = await _env.probe_module.call_command("ocpp", "restart", None) + assert res is True + assert csms_connection.is_connected + + async def test_command_restart_denied(self, _env): + csms_connection = CSMSConnectionUtils(_env.central_system) + res = await _env.probe_module.call_command("ocpp", "restart", None) + assert res is False + assert csms_connection.is_connected + + async def test_command_security_event(self, _env): + res = await _env.probe_module.call_command( + "ocpp", + "security_event", + { + "event": { + "type": "SecurityLogWasCleared", + "info": "integration_test_security_info", + "critical": True, + "timestamp": "2024-01-01T12:00:00", + } + }, + ) + assert res is None + await wait_for_mock_called( + _env.csms_mock.on_security_event_notification, + mock_call( + tech_info="integration_test_security_info", + timestamp=ANY, + type="SecurityLogWasCleared", + ), + ) + assert ( + len(_env.csms_mock.on_security_event_notification.mock_calls) == 2 + ) # we expect 2 because of the StartupOfTheDevice + + @pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment( + [("Custom", "ExampleConfigurationKey", "test_value")] + ) + ) + async def test_command_get_variables(self, _env): + res = await _env.probe_module.call_command( + "ocpp", + "get_variables", + { + "requests": [ + { + "component_variable": { + "component": {"name": "IGNORED"}, + "variable": {"name": "ChargePointId"}, + } + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "UNKNOWN"}, + }, + "attribute_type": "Target", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": { + "name": "ExampleConfigurationKey", + "instance": "TO_BE_IGNORED", + }, + }, + "attribute_type": "Target", # ignored + }, + ] + }, + ) + + assert res == [ + { + "attribute_type": "Actual", + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "ChargePointId"}, + }, + "status": "Accepted", + "value": "cp001", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "UNKNOWN"}, + }, + "status": "UnknownVariable", + }, + { + "attribute_type": "Actual", + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "ExampleConfigurationKey"}, + }, + "status": "Accepted", + "value": "test_value", + }, + ] + + @pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment( + [("Custom", "ExampleConfigurationKey", "test_value")] + ) + ) + async def test_command_set_variables(self, _env): + res = await _env.probe_module.call_command( + "ocpp", + "set_variables", + { + "requests": [ + { + "component_variable": { + "component": {"name": "IGNORED"}, + "variable": {"name": "RetryBackoffRandomRange"}, + }, + # not custom - will be rejectged + "value": "99", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "UNKNOWN"}, + }, + "attribute_type": "Target", + "value": "test_value", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": { + "name": "ExampleConfigurationKey", + "instance": "TO_BE_IGNORED", + }, + }, + "attribute_type": "Target", + "value": "unittest changed value", + }, + ], + "source": "testcase", + }, + ) + + assert res + assert isinstance(res, list) and len(res) == 3 + assert res == [ + { + "component_variable": { + "component": {"name": "IGNORED"}, + "variable": {"name": "RetryBackoffRandomRange"}, + }, + "status": "Rejected", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "UNKNOWN"}, + }, + "status": "Rejected", + }, + { + "component_variable": { + "component": {"name": ""}, + "variable": { + "instance": "TO_BE_IGNORED", + "name": "ExampleConfigurationKey", + }, + }, + "status": "Accepted", + }, + ] + + # Verify value changed + check = await _env.probe_module.call_command( + "ocpp", + "get_variables", + { + "requests": [ + { + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "ExampleConfigurationKey"}, + } + } + ] + }, + ) + assert check == [ + { + "attribute_type": "Actual", + "component_variable": { + "component": {"name": ""}, + "variable": {"name": "ExampleConfigurationKey"}, + }, + "status": "Accepted", + "value": "unittest changed value", + } + ] + + async def test_command_monitor_variables(self, _env): + """Test monitoring a configuraton variable as well as an event_data subscription.""" + + async def change_var(key: str, value: str): + res = await _env.charge_point.change_configuration_req(key=key, value=value) + assert res.status == "Accepted" + + event_data_subscription_mock = Mock() + _env.probe_module.subscribe_variable( + "ocpp", "event_data", event_data_subscription_mock + ) + + await change_var("HeartbeatInterval", "1") + + # assert no event before monitoring is enabled + await asyncio.sleep(0.1) + event_data_subscription_mock.assert_not_called() + + # enable monitoring + res = await _env.probe_module.call_command( + "ocpp", + "monitor_variables", + { + "component_variables": [ + { + "component": {"name": "IGNORED"}, + "variable": {"name": "HeartbeatInterval"}, + }, + { + "component": {"name": ""}, + "variable": {"name": "MeterValuesAlignedData"}, + }, + { + "component": {"name": ""}, + "variable": {"name": "UNKNOWN"}, + }, + ] + }, + ) + assert res is None + + # verify event is triggered + await change_var("HeartbeatInterval", "42") + await wait_for_mock_called( + event_data_subscription_mock, + mock_call( + { + "actual_value": "42", + "component_variable": { + "component": {"name": "IGNORED"}, + "variable": {"name": "HeartbeatInterval"}, + }, + "event_id": ANY, + "event_notification_type": "CustomMonitor", + "timestamp": ANY, + "trigger": "Alerting", + } + ), + ) + + async def test_subscribe_charging_schedules(self, _env): + subscription_mock = Mock() + _env.probe_module.subscribe_variable( + "ocpp", "charging_schedules", subscription_mock + ) + + await _env.charge_point.set_charging_profile_req( + SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles={ + "chargingProfileId": 0, + "stackLevel": 1, + "chargingProfilePurpose": "TxDefaultProfile", + "chargingProfileKind": "Relative", + "chargingSchedule": { + "chargingRateUnit": "A", + "chargingSchedulePeriod": [{"limit": 32.0, "startPeriod": 0}], + }, + }, + ) + ) + await wait_for_mock_called( + subscription_mock, + mock_call( + { + "schedules": [ + { + "charging_rate_unit": "A", + "charging_schedule_period": [ + { + "limit": 48, + "number_phases": 3, + "stack_level": 0, + "start_period": 0, + } + ], + "evse": 0, + "duration": ANY, + "start_schedule": ANY, + }, + { + "charging_rate_unit": "A", + "charging_schedule_period": [ + { + "limit": 32, + "number_phases": 3, + "stack_level": 1, + "start_period": 0, + } + ], + "evse": 1, + "duration": ANY, + "start_schedule": ANY, + }, + { + "charging_rate_unit": "A", + "charging_schedule_period": [ + { + "limit": 32, + "number_phases": 3, + "stack_level": 1, + "start_period": 0, + } + ], + "evse": 2, + "duration": ANY, + "start_schedule": ANY, + }, + ] + } + ), + ) + + async def test_subscribe_is_connected(self, _env): + subscription_mock = Mock() + _env.probe_module.subscribe_variable("ocpp", "is_connected", subscription_mock) + + assert await _env.probe_module.call_command("ocpp", "stop", None) + assert await _env.probe_module.call_command("ocpp", "restart", None) + + await wait_for_mock_called(subscription_mock, mock_call(False)) + await wait_for_mock_called(subscription_mock, mock_call(True)) + + @pytest.mark.parametrize( + "overwrite_implementation", + [{"security": {"update_leaf_certificate": "InvalidSignature"}}], + ) + async def test_subscribe_security_event(self, _env): + subscription_mock = Mock() + _env.probe_module.subscribe_variable( + "ocpp", "security_event", subscription_mock + ) + # trigger security event by invalid certificate signed request + await _env.charge_point.certificate_signed_req(certificate_chain="somechain") + + await wait_for_mock_called( + subscription_mock, + mock_call( + {"info": "InvalidSignature", "type": "InvalidChargePointCertificate"} + ), + ) + + async def test_change_availability_request_connector(self, _env): + _env.probe_module_command_mocks["evse_manager"]["enable_disable"].reset_mock() + + res = await _env.probe_module.call_command( + "ocpp", + "change_availability", + { + "request": { + "operational_status": "Inoperative", + "evse": { + "id": 1, + "connector_id": 1, + }, + } + }, + ) + assert res == {"status": "Accepted"} + + await wait_for_mock_called( + _env.probe_module_command_mocks["evse_manager"]["enable_disable"], + call=mock_call( + { + "cmd_source": { + "enable_priority": 5000, + "enable_source": "CSMS", + "enable_state": "Disable", + }, + "connector_id": 0, + } + ), + ) # as currently implemented in disable_evse callback in OCPP module + + _env.probe_module_command_mocks["evse_manager"]["enable_disable"].reset_mock() + _env.probe_module_command_mocks["evse_manager_b"]["enable_disable"].reset_mock() + + res = await _env.probe_module.call_command( + "ocpp", + "change_availability", + { + "request": { + "operational_status": "Inoperative", + "evse": { + "id": 2, + "connector_id": 1, + }, + } + }, + ) + assert res == {"status": "Accepted"} + + await wait_for_mock_called( + _env.probe_module_command_mocks["evse_manager_b"]["enable_disable"], + call=mock_call( + { + "cmd_source": { + "enable_priority": 5000, + "enable_source": "CSMS", + "enable_state": "Disable", + }, + "connector_id": 0, + } + ), + ) # as currently implemented in disable_evse callback in OCPP module + + async def test_change_availability_request_evse(self, _env): + _env.probe_module_command_mocks["evse_manager"]["enable_disable"].reset_mock() + + res = await _env.probe_module.call_command( + "ocpp", + "change_availability", + {"request": {"operational_status": "Inoperative"}}, + ) + assert res == {"status": "Accepted"} + await wait_for_mock_called( + _env.probe_module_command_mocks["evse_manager"]["enable_disable"], + call=mock_call( + { + "cmd_source": { + "enable_priority": 5000, + "enable_source": "CSMS", + "enable_state": "Disable", + }, + "connector_id": 0, + } + ), + ) + assert ( + len( + _env.probe_module_command_mocks["evse_manager"][ + "enable_disable" + ].mock_calls + ) + == 1 + ) + + async def test_change_availability_request_failed(self, _env): + # Failed request: no connector id + res = await _env.probe_module.call_command( + "ocpp", + "change_availability", + { + "request": { + "operational_status": "Inoperative", + "evse": { + "id": 1, + }, + } + }, + ) + assert res == { + "status": "Rejected", + "status_info": { + "additional_info": ANY, # No connector id specified; + "reason_code": "InvalidInput", + }, + } + + res = await _env.probe_module.call_command( + "ocpp", + "change_availability", + { + "request": { + "operational_status": "Inoperative", + "evse": {"id": 2, "connector_id": 2}, + } + }, + ) + assert res == { + "status": "Rejected", + "status_info": { + "additional_info": ANY, # Invalid connector id specified + "reason_code": "InvalidInput", + }, + } diff --git a/tests/ocpp_tests/test_sets/ocpp16/plug_and_charge_tests.py b/tests/ocpp_tests/test_sets/ocpp16/plug_and_charge_tests.py new file mode 100644 index 000000000..057243a09 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/plug_and_charge_tests.py @@ -0,0 +1,664 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +# fmt: off +import os +import sys + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__), "../.."))) +from everest.testing.ocpp_utils.fixtures import * +from ocpp.v201.enums import (CertificateSigningUseType) +from ocpp.v201 import call as call201 +from ocpp.v16.enums import ChargePointErrorCode, ChargePointStatus +from ocpp.v16 import call +from ocpp.charge_point import asdict, remove_nones, snake_to_camel_case, camel_to_snake_case +from ocpp.routing import create_route_map +import asyncio +import pytest +from validations import (validate_standard_start_transaction, + validate_data_transfer_pnc_get_15118_ev_certificate, + validate_data_transfer_sign_certificate) +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 +from everest_test_utils import * +# fmt: on + + +def validate_authorize_req( + authorize_req: call201.AuthorizePayload, contains_contract, contains_ocsp +): + return (authorize_req.certificate != None) == contains_contract and ( + authorize_req.iso15118_certificate_hash_data != None + ) == contains_ocsp + + +@pytest.mark.skip( + "Plug and charge tests do currently interfere when they are run in parallel with other tests" +) +class TestPlugAndCharge: + + @pytest.mark.asyncio + @pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs") + async def test_contract_installation_and_authorization_01( + self, + request, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_config, + test_utility: TestUtility, + ): + """ + Test for contract installation on the vehicle and succeeding authorization and charging process + """ + + setattr(charge_point_v16, "on_data_transfer", on_data_transfer_accept_authorize) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + await asyncio.sleep(3) + test_controller.plug_in_ac_iso() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + call.DataTransferPayload( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="Get15118EVCertificate", + data=None, + ), + validate_data_transfer_pnc_get_15118_ev_certificate, + ) + + # expect authorize.req + r: call.DataTransferPayload = call.DataTransferPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + { + "messageId": "Authorize", + "vendorId": "org.openchargealliance.iso15118pnc", + }, + ) + ) + + authorize_req: call201.AuthorizePayload = call201.AuthorizePayload( + **camel_to_snake_case(json.loads(r.data)) + ) + assert validate_authorize_req(authorize_req, False, True) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.emaid, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + async def test_contract_installation_and_authorization_02( + self, + request, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, + ): + """ + Test for contract installation on the vehicle and succeeding authorization request that is rejected by CSMS + """ + + setattr(charge_point_v16, "on_data_transfer", on_data_transfer_reject_authorize) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + await asyncio.sleep(3) + test_controller.plug_in_ac_iso() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + call.DataTransferPayload( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="Get15118EVCertificate", + data=None, + ), + validate_data_transfer_pnc_get_15118_ev_certificate, + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + { + "messageId": "Authorize", + "vendorId": "org.openchargealliance.iso15118pnc", + }, + ) + + test_utility.messages.clear() + test_utility.forbidden_actions.append("StartTransaction") + + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + @pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs") + async def test_contract_installation_and_authorization_03( + self, + request, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_config, + test_utility: TestUtility, + ): + """ + Test for contract installation on the vehicle and succeeding authorization and charging process + """ + + await charge_point_v16.change_configuration_req( + key="CentralContractValidationAllowed", value="true" + ) + + certificate_hash_data = { + "hashAlgorithm": "SHA256", + "issuerKeyHash": "66fce9295edc049f4a183458948ecaa8e3558e4aa3041f13a2363d1d953d33e5", + "issuerNameHash": "3a1ad85a129bd5db30c2f099a541f76e562b8a30e9f49f3f47077eeae3750a2a", + "serialNumber": "3041", + } + + delete_certificate_req = {"certificateHashData": certificate_hash_data} + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="DeleteCertificate", + data=json.dumps(delete_certificate_req), + ) + + # expect not found + assert json.loads(data_transfer_response.data) == {"status": "Accepted"} + + setattr(charge_point_v16, "on_data_transfer", on_data_transfer_accept_authorize) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + test_controller.plug_in_ac_iso() + # expect authorize.req + r: call.DataTransferPayload = call.DataTransferPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + { + "messageId": "Authorize", + "vendorId": "org.openchargealliance.iso15118pnc", + }, + ) + ) + + authorize_req: call201.AuthorizePayload = call201.AuthorizePayload( + **camel_to_snake_case(json.loads(r.data)) + ) + assert validate_authorize_req(authorize_req, True, False) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.emaid, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + async def test_eim_01( + self, + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, + ): + """ + Test normal EIM authentication with swipe first. + We should test that: + - Charging process starts + - DataTransfer(Authorize.req) is not transmitted in this case. + """ + + test_utility.forbidden_actions.append("DataTransfer") + + # swipe first + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # plug in second + test_controller.plug_in_ac_iso() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await asyncio.sleep(10) + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + @pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-sil-iso no-tls.yaml") + ) + async def test_eim_02( + self, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, + ): + """ + Test normal EIM authentication with plugin first and autocharge. + We should test that: + - Charging process starts + - DataTransfer(Authorize.req) is not transmitted in this case. + """ + + # plug in + test_controller.plug_in_ac_iso() + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload(1, None, 0, ""), + validate_standard_start_transaction, + ) + + start_transaction_req = test_utility.messages.pop() + assert "VID" in start_transaction_req.payload["idTag"] + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.charging + ), + ) + + await asyncio.sleep(10) + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + async def test_pnc_reject( + self, + test_config, + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, + ): + """ + CSMS rejects DataTransfer(Authorize.req) from CP + Charging process should not start, even when a valid RFID is presented. + """ + + setattr(charge_point_v16, "on_data_transfer", on_data_transfer_reject_authorize) + central_system_v16.chargepoint.route_map = create_route_map( + central_system_v16.chargepoint + ) + + test_utility.forbidden_actions.append("StartTransaction") + + # plug in first + test_controller.plug_in_ac_iso() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.preparing + ), + ) + + # expect authorize.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + { + "messageId": "Authorize", + "vendorId": "org.openchargealliance.iso15118pnc", + }, + ) + + test_utility.messages.clear() + + # swipe second + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + test_utility.messages.clear() + test_controller.plug_out_iso() + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.available + ), + ) + + @pytest.mark.asyncio + async def test_eim_and_autocharge(self, charge_point_v16: ChargePoint16): + pass + + @pytest.mark.asyncio + async def test_eim_only(self, charge_point_v16: ChargePoint16): + pass + + @pytest.mark.asyncio + async def test_pnc_certificate_signed_01(self, charge_point_v16: ChargePoint16): + """ + Test with invalid certificate chain + """ + certificate_signed_req = {"certificateChain": "InvalidCertificateChain"} + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="CertificateSigned", + data=json.dumps(certificate_signed_req), + ) + + assert json.loads(data_transfer_response.data) == {"status": "Rejected"} + assert data_transfer_response.status == "Accepted" + + @pytest.mark.asyncio + async def test_pnc_delete_certificate(self, charge_point_v16: ChargePoint16): + """ + Test delete certificate. Test with valid and invalid CertificateHashData + """ + certificate_hash_data = { + "hashAlgorithm": "SHA256", + "issuerKeyHash": "XXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "issuerNameHash": "YYYYYYYYYYYYYYYYYYYYYYYYYYY", + "serialNumber": "1", + } + delete_certificate_req = {"certificateHashData": certificate_hash_data} + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="DeleteCertificate", + data=json.dumps(delete_certificate_req), + ) + + # expect not found + assert json.loads(data_transfer_response.data) == {"status": "NotFound"} + assert data_transfer_response.status == "Accepted" + + certificate_hash_data["hashAlgorithm"] = "SHA_Invalid" + delete_certificate_req = {"certificateHashData": certificate_hash_data} + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="DeleteCertificate", + data=json.dumps(delete_certificate_req), + ) + + # expect rejected because of invalid hash algorithm + assert data_transfer_response.status == "Rejected" + + @pytest.mark.asyncio + async def test_pnc_get_15118_ev_certificate(self): + pass + + @pytest.mark.asyncio + @pytest.mark.skip("Test does nothing yet") + async def test_pnc_get_certificate_status(self, charge_point_v16: ChargePoint16): + await asyncio.sleep(30) + + @pytest.mark.asyncio + async def test_pnc_get_installed_certificate_ids( + self, charge_point_v16: ChargePoint16 + ): + """ + Test get installed certificate ids. Test with valid and invalid request + """ + get_installed_certificate_ids_req = {"certificateType": ["MORootCertificate"]} + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="GetInstalledCertificateIds", + data=json.dumps(get_installed_certificate_ids_req), + ) + + assert "status" in json.loads(data_transfer_response.data) + + get_installed_certificate_ids_req = { + "certificateType": ["ManufacturerRootCertificate"] + } + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="GetInstalledCertificateIds", + data=json.dumps(get_installed_certificate_ids_req), + ) + + assert "status" in json.loads(data_transfer_response.data) + + get_installed_certificate_ids_req = { + "certificateType": ["InvalidRootCertificateType"] + } + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="GetInstalledCertificateIds", + data=json.dumps(get_installed_certificate_ids_req), + ) + + assert data_transfer_response.status == "Rejected" + + @pytest.mark.asyncio + async def test_pnc_install_certificate( + self, request, charge_point_v16: ChargePoint16 + ): + + v2g_root_ca_path = ( + Path(request.config.getoption("--everest-prefix")) + / "etc/everest/certs/ca/v2g/V2G_ROOT_CA.pem" + ) + try: + os.remove(v2g_root_ca_path) + except Exception: + print(f"Could not remove dir: {v2g_root_ca_path}") + + with open( + Path(os.path.dirname(__file__)).parent + / "everest-aux/certs/ca/v2g/V2G_ROOT_CA.pem", + "r", + ) as f: + v2g_certificate_chain = f.read() + + install_certificate_req = { + "certificateType": "V2GRootCertificate", + "certificate": v2g_certificate_chain, + } + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="InstallCertificate", + data=json.dumps(install_certificate_req), + ) + + assert data_transfer_response.status == "Accepted" + assert json.loads(data_transfer_response.data) == {"status": "Accepted"} + + install_certificate_req["certificate"] = "InvalidCertificate" + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="InstallCertificate", + data=json.dumps(install_certificate_req), + ) + + assert data_transfer_response.status == "Accepted" + assert json.loads(data_transfer_response.data) == {"status": "Rejected"} + + @pytest.mark.asyncio + async def test_pnc_sign_certificate_and_trigger_message( + self, test_utility, charge_point_v16: ChargePoint16 + ): + trigger_message_req = {"requestedMessage": "SignCertificate"} + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="TriggerMessage", + data=json.dumps(trigger_message_req), + ) + + assert data_transfer_response.status == "Accepted" + assert json.loads(data_transfer_response.data) == {"status": "Accepted"} + + csr = await wait_for_and_validate( + test_utility, + charge_point_v16, + "DataTransfer", + call.DataTransferPayload( + data=json.dumps( + remove_nones( + snake_to_camel_case( + asdict( + call201.SignCertificatePayload( + csr="", + certificate_type=CertificateSigningUseType.v2g_certificate, + ) + ) + ) + ), + separators=(",", ":"), + ), + message_id="SignCertificate", + vendor_id="org.openchargealliance.iso15118pnc", + ), + validate_data_transfer_sign_certificate, + ) + + assert csr + certificate_signed = certificate_signed_response(csr) + certificate_signed_req = {"certificateChain": certificate_signed} + + data_transfer_response = await charge_point_v16.data_transfer_req( + vendor_id="org.openchargealliance.iso15118pnc", + message_id="CertificateSigned", + data=json.dumps(certificate_signed_req), + ) + + assert json.loads(data_transfer_response.data) == {"status": "Accepted"} + assert data_transfer_response.status == "Accepted" diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/absolute_profiles.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/absolute_profiles.py new file mode 100644 index 000000000..ec6e1bc3b --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/absolute_profiles.py @@ -0,0 +1,451 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta + +from ocpp.v16.enums import GetCompositeScheduleStatus +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result + + +def abs_req1_test1(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=86400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10) + ], + ), + ), + ) + + +def abs_req2_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ChargingSchedulePeriod(start_period=180, limit=25), + ChargingSchedulePeriod(start_period=260, limit=8), + ], + ), + ), + ) + + +def abs_req3_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=240, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=8), + ChargingSchedulePeriod(start_period=50, limit=11), + ChargingSchedulePeriod(start_period=140, limit=16), + ChargingSchedulePeriod(start_period=200, limit=6), + ChargingSchedulePeriod(start_period=240, limit=12), + ], + ), + ), + ) + + +def abs_exp_test1(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=8, number_phases=3), + ChargingSchedulePeriod( + start_period=50 - passed_seconds, limit=10, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=200 - passed_seconds, limit=6, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=240 - passed_seconds, limit=10, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=260 - passed_seconds, limit=8, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=300 - passed_seconds, limit=10, number_phases=3 + ), + ], + ), + ) + + +def abs_req1_test2(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6, number_phases=3), + ChargingSchedulePeriod(start_period=60, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=120, limit=8, number_phases=3), + ], + ), + ), + ) + + +def abs_exp_test2(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6, number_phases=3), + ChargingSchedulePeriod( + start_period=60 - passed_seconds, limit=10, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=120 - passed_seconds, limit=8, number_phases=3 + ), + ], + ), + ) + + +def abs_req1_test3(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + transaction_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=260, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + +def abs_exp_test3(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6, number_phases=3), + ChargingSchedulePeriod( + start_period=60 - passed_seconds, limit=10, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=120 - passed_seconds, limit=8, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=260 - passed_seconds, limit=48, number_phases=3 + ), + ], + ), + ) + + +def abs_req1_test5(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6), + ChargingSchedulePeriod(start_period=50, limit=5), + ChargingSchedulePeriod(start_period=100, limit=8), + ChargingSchedulePeriod(start_period=200, limit=10), + ], + ), + ), + ) + + +def abs_req2_test5(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=150, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=7), + ChargingSchedulePeriod(start_period=100, limit=9), + ], + ), + ), + ) + + +def abs_exp_test5_1(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=350, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=7, number_phases=3), + ChargingSchedulePeriod( + start_period=100 - passed_seconds, limit=9, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=150 - passed_seconds, limit=8, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=200 - passed_seconds, limit=10, number_phases=3 + ), + ], + ), + ) + + +def abs_exp_test5_2(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=550, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=7, number_phases=3), + ChargingSchedulePeriod( + start_period=100 - passed_seconds, limit=9, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=150 - passed_seconds, limit=8, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=200 - passed_seconds, limit=10, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=400 - passed_seconds, limit=48, number_phases=3 + ), + ], + ), + ) + + +def abs_req1_test6(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=800, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=18), + ChargingSchedulePeriod(start_period=50, limit=24), + ChargingSchedulePeriod(start_period=100, limit=14), + ChargingSchedulePeriod(start_period=200, limit=24), + ], + ), + ), + ) + + +def abs_req2_test6(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16), + ChargingSchedulePeriod(start_period=100, limit=20), + ], + ), + ), + ) + + +def abs_req3_test6(): + return call.SetChargingProfilePayload( + connector_id=2, + cs_charging_profiles=ChargingProfile( + charging_profile_id=3, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=500, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), + ChargingSchedulePeriod(start_period=100, limit=16), + ], + ), + ), + ) + + +def abs_exp_test6_con0(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=0, + charging_schedule=ChargingSchedule( + duration=700, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=18, number_phases=3), + ChargingSchedulePeriod(start_period=50, limit=24, number_phases=3), + ChargingSchedulePeriod(start_period=100, limit=14, number_phases=3), + ChargingSchedulePeriod(start_period=200, limit=24, number_phases=3), + ], + ), + ) + + +def abs_exp_test6_con1(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=900, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16, number_phases=3), + ChargingSchedulePeriod( + start_period=100 - passed_seconds, limit=14, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=200 - passed_seconds, limit=20, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=400 - passed_seconds, limit=24, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=800 - passed_seconds, limit=48, number_phases=3 + ), + ], + ), + ) + + +def abs_exp_test6_con2(passed_seconds): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=2, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod( + start_period=100 - passed_seconds, limit=14, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=200 - passed_seconds, limit=16, number_phases=3 + ), + ], + ), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/combined_profiles.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/combined_profiles.py new file mode 100644 index 000000000..088821c63 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/combined_profiles.py @@ -0,0 +1,151 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta + +from ocpp.v16.enums import GetCompositeScheduleStatus +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result + + +def comb_req1_test1(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=200, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), # 6900 + ChargingSchedulePeriod( + start_period=80, limit=20, number_phases=1 # 4600 + ), + ChargingSchedulePeriod( + start_period=100, limit=20, number_phases=3 # 13800 + ), + ], + ), + ), + ) + + +def comb_req2_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.watts, + charging_schedule_period=[ + ChargingSchedulePeriod( + start_period=0, limit=11000, number_phases=3 + ), + ChargingSchedulePeriod( + start_period=60, limit=6900, number_phases=1 + ), + ChargingSchedulePeriod(start_period=120, limit=5520), + ChargingSchedulePeriod(start_period=180, limit=17250), + ChargingSchedulePeriod(start_period=260, limit=5520), + ], + ), + ), + ) + + +def comb_exp1_test1(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.watts, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=6900, number_phases=3), + ChargingSchedulePeriod(start_period=80, limit=4600, number_phases=1), + ChargingSchedulePeriod(start_period=100, limit=6900, number_phases=1), + ChargingSchedulePeriod(start_period=120, limit=5520, number_phases=3), + ChargingSchedulePeriod(start_period=180, limit=13800, number_phases=3), + ChargingSchedulePeriod(start_period=200, limit=17250, number_phases=3), + ChargingSchedulePeriod(start_period=260, limit=5520, number_phases=3), + ChargingSchedulePeriod(start_period=300, limit=33120, number_phases=3), + ], + ), + ) + + +def comb_exp2_test1(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=80, limit=20, number_phases=1), + ChargingSchedulePeriod(start_period=100, limit=30, number_phases=1), + ChargingSchedulePeriod(start_period=120, limit=8, number_phases=3), + ChargingSchedulePeriod(start_period=180, limit=20, number_phases=3), + ChargingSchedulePeriod(start_period=200, limit=25, number_phases=3), + ChargingSchedulePeriod(start_period=260, limit=8, number_phases=3), + ChargingSchedulePeriod(start_period=300, limit=48, number_phases=3), + ], + ), + ) + + +def comb_req1_test2(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + charging_schedule=ChargingSchedule( + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), + ChargingSchedulePeriod(start_period=80, limit=20, number_phases=2), + ChargingSchedulePeriod(start_period=160, limit=20, number_phases=3), + ], + ), + ), + ) + + +def comb_exp_test2(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=0, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=80, limit=20, number_phases=2), + ChargingSchedulePeriod(start_period=160, limit=20, number_phases=3), + ], + ), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/recurring_profiles.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/recurring_profiles.py new file mode 100644 index 000000000..1b03b11c0 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/recurring_profiles.py @@ -0,0 +1,167 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta + +from ocpp.v16.enums import GetCompositeScheduleStatus +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result + + +def rec_req1_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.recurring, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16), + ChargingSchedulePeriod(start_period=100, limit=20), + ], + ), + ), + ) + + +def rec_req1_test2(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.recurring, + recurrency_kind=RecurrencyKind.daily, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=14), + ChargingSchedulePeriod(start_period=5000, limit=16), + ChargingSchedulePeriod(start_period=15000, limit=20), + ], + ), + ), + ) + + +def rec_req2_test2(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.recurring, + recurrency_kind=RecurrencyKind.daily, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + start_schedule=datetime.utcnow().isoformat(), + duration=86400, + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), + ChargingSchedulePeriod(start_period=10000, limit=22), + ChargingSchedulePeriod(start_period=20000, limit=6), + ], + ), + ), + ) + + +def rec_exp_test2(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=172800, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=10000, limit=22, number_phases=3), + ChargingSchedulePeriod(start_period=20000, limit=6, number_phases=3), + ChargingSchedulePeriod(start_period=86400, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=96400, limit=22, number_phases=3), + ChargingSchedulePeriod(start_period=106400, limit=6, number_phases=3), + ], + ), + ) + + +def rec_req1_test3(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.recurring, + recurrency_kind=RecurrencyKind.weekly, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=14), + ChargingSchedulePeriod(start_period=5000, limit=16), + ChargingSchedulePeriod(start_period=15000, limit=20), + ], + ), + ), + ) + + +def rec_req2_test3(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.recurring, + recurrency_kind=RecurrencyKind.weekly, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + start_schedule=datetime.utcnow().isoformat(), + duration=86400, + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), + ChargingSchedulePeriod(start_period=10000, limit=22), + ChargingSchedulePeriod(start_period=20000, limit=6), + ], + ), + ), + ) + + +def rec_exp_test3(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=172800, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=10000, limit=22, number_phases=3), + ChargingSchedulePeriod(start_period=20000, limit=6, number_phases=3), + ChargingSchedulePeriod(start_period=86400, limit=20, number_phases=3), + ], + ), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/relative_profiles.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/relative_profiles.py new file mode 100644 index 000000000..ce6140d43 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/relative_profiles.py @@ -0,0 +1,168 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta + +from ocpp.v16.enums import GetCompositeScheduleStatus +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result + + +def rel_req1_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + transaction_id=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.relative, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16), + ChargingSchedulePeriod(start_period=50, limit=20), + ], + ), + ), + ) + + +def rel_exp_test1(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16, number_phases=3), + ChargingSchedulePeriod(start_period=50, limit=20, number_phases=3), + ], + ), + ) + + +def rel_req1_test2(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.relative, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=16), + ChargingSchedulePeriod(start_period=100, limit=20), + ], + ), + ), + ) + + +def rel_req2_test2(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.relative, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=200, + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10), + ChargingSchedulePeriod(start_period=50, limit=6), + ], + ), + ), + ) + + +def rel_exp_test2(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=50, limit=6, number_phases=3), + ChargingSchedulePeriod(start_period=200, limit=20, number_phases=3), + ], + ), + ) + + +def rel_req1_test3(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=1, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.relative, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + charging_rate_unit=ChargingRateUnitType.watts, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=11000), + ChargingSchedulePeriod(start_period=90, limit=22000), + ], + ), + ), + ) + + +def rel_req2_test3(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=2, + stack_level=2, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.relative, + valid_from=datetime.utcnow().isoformat(), + charging_schedule=ChargingSchedule( + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=32), + ChargingSchedulePeriod(start_period=6, limit=20), + ChargingSchedulePeriod(start_period=12, limit=8), + ], + ), + ), + ) + + +def rel_exp_test3(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=90, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=32, number_phases=3), + ChargingSchedulePeriod(start_period=6, limit=20, number_phases=3), + ChargingSchedulePeriod(start_period=12, limit=8, number_phases=3), + ], + ), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/unplausable_profiles.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/unplausable_profiles.py new file mode 100644 index 000000000..9f07c3a65 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_profiles/unplausable_profiles.py @@ -0,0 +1,102 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta + +from ocpp.v16.enums import GetCompositeScheduleStatus +from ocpp.v16.datatypes import * +from ocpp.v16 import call, call_result + + +def unp_req1_test1(): + return call.SetChargingProfilePayload( + connector_id=0, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.charge_point_max_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=86400, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=10) + ], + ), + ), + ) + + +def unp_req2_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_default_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod( + start_period=0, + limit=6, + ), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=8), + ], + ), + ), + ) + + +def unp_req3_test1(): + return call.SetChargingProfilePayload( + connector_id=1, + cs_charging_profiles=ChargingProfile( + charging_profile_id=1, + stack_level=0, + charging_profile_purpose=ChargingProfilePurposeType.tx_profile, + charging_profile_kind=ChargingProfileKindType.absolute, + valid_from=datetime.utcnow().isoformat(), + valid_to=(datetime.utcnow() + timedelta(days=3)).isoformat(), + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod( + start_period=0, + limit=12, + ), + ChargingSchedulePeriod(start_period=60, limit=10), + ChargingSchedulePeriod(start_period=120, limit=6), + ], + ), + ), + ) + + +def unp_exp_test1(): + return call_result.GetCompositeSchedulePayload( + status=GetCompositeScheduleStatus.accepted, + schedule_start=datetime.utcnow().isoformat(), + connector_id=1, + charging_schedule=ChargingSchedule( + duration=300, + start_schedule=datetime.utcnow().isoformat(), + charging_rate_unit=ChargingRateUnitType.amps, + charging_schedule_period=[ + ChargingSchedulePeriod(start_period=0, limit=12, number_phases=3), + ChargingSchedulePeriod(start_period=60, limit=10, number_phases=3), + ChargingSchedulePeriod(start_period=120, limit=6, number_phases=3), + ], + ), + ) diff --git a/tests/ocpp_tests/test_sets/ocpp16/smart_charging_tests.py b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_tests.py new file mode 100644 index 000000000..f45486336 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/smart_charging_tests.py @@ -0,0 +1,818 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +import asyncio +from datetime import datetime + +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +from ocpp.v16.datatypes import ( + ChargingRateUnitType, +) + +from ocpp.v16.enums import ChargingProfileStatus, RemoteStartStopStatus + +from ocpp.v16 import call, call_result + +# fmt: off +from validations import (validate_composite_schedule, + validate_remote_start_stop_transaction, + validate_standard_start_transaction) + +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16 + +from smart_charging_profiles.absolute_profiles import * +from smart_charging_profiles.relative_profiles import * +from smart_charging_profiles.recurring_profiles import * +from smart_charging_profiles.combined_profiles import * +from everest_test_utils import * +# fmt: on + + +@pytest.mark.asyncio +async def test_absolute_1( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + req1 = abs_req1_test1() + req2 = abs_req2_test1() + req3 = abs_req3_test1() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req3) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=400) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req1.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test1(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_absolute_2( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req = abs_req1_test2() + + assert await charge_point_v16.set_charging_profile_req(req) == exp_scp_result + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=300) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test2(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_absolute_3( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + req = abs_req1_test3() + + assert await charge_point_v16.set_charging_profile_req(req) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=300) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test3(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + test_controller.plug_out() + + await asyncio.sleep(2) + + +@pytest.mark.asyncio +async def test_absolute_4( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + req = abs_req1_test2() + + assert await charge_point_v16.set_charging_profile_req(req) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=300) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test2(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_absolute_5( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req1 = abs_req1_test5() + req2 = abs_req2_test5() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=350) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req1.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test5_1(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + test_utility.messages.clear() + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=550) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req1.cs_charging_profiles.valid_from) + ).total_seconds() + ) + exp = abs_exp_test5_2(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-two-connectors.yaml") +) +@pytest.mark.asyncio +async def test_absolute_6( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req1 = abs_req1_test6() + req2 = abs_req2_test6() + req3 = abs_req3_test6() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req3) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=0, duration=700) + ) + + exp_con0 = abs_exp_test6_con0() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_con0, + validate_composite_schedule, + ) + + test_utility.messages.clear() + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=0, duration=700) + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_con0, + validate_composite_schedule, + ) + + test_utility.messages.clear() + + test_controller.plug_in(connector_id=1) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=900) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req1.cs_charging_profiles.valid_from) + ).total_seconds() + ) + + exp_con1 = abs_exp_test6_con1(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_con1, + validate_composite_schedule, + ) + + test_utility.messages.clear() + + test_controller.plug_in(connector_id=2) + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_2, connector_id=2 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 2, test_config.authorization_info.valid_id_tag_2, 0, "" + ), + validate_standard_start_transaction, + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=2, duration=400) + ) + + passed_seconds = int( + ( + datetime.utcnow() + - datetime.fromisoformat(req2.cs_charging_profiles.valid_from) + ).total_seconds() + ) + + exp_con2 = abs_exp_test6_con2(passed_seconds) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp_con2, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +@pytest.mark.skip( + "Expected behavior when schedules are sent in other unit than composite schedules are requested needs to be discussed." +) +async def test_combined_1( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req1 = comb_req1_test1() + req2 = comb_req2_test1() + exp1 = comb_exp1_test1() + exp2 = comb_exp2_test1() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload( + connector_id=1, duration=400, charging_rate_unit=ChargingRateUnitType.watts + ) + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload( + connector_id=1, duration=400, charging_rate_unit=ChargingRateUnitType.amps + ) + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp1, + validate_composite_schedule, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp2, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_combined_2(charge_point_v16: ChargePoint16, test_utility: TestUtility): + + req = comb_req1_test2() + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + assert await charge_point_v16.set_charging_profile_req(req) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=0, duration=400) + ) + + exp = comb_exp_test2() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_recurring_1( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + req = rec_req1_test1() + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.rejected + ) + + assert await charge_point_v16.set_charging_profile_req(req) == exp_scp_result + + +@pytest.mark.asyncio +async def test_recurring_2( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req1 = rec_req1_test2() + req2 = rec_req2_test2() + exp = rec_exp_test2() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=172800) + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_recurring_3( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + req1 = rec_req1_test3() + req2 = rec_req2_test3() + exp = rec_exp_test3() + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=172800) + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_relative_1( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + req1 = rel_req1_test1() + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=400) + ) + + exp = rel_exp_test1() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_relative_2( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + req1 = rel_req1_test2() + req2 = rel_req2_test2() + + test_controller.plug_in() + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=300) + ) + + exp = rel_exp_test2() + + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) + + +@pytest.mark.asyncio +async def test_relative_3( + test_config, + charge_point_v16: ChargePoint16, + test_controller: TestController, + test_utility: TestUtility, +): + + req1 = rel_req1_test3() + req2 = rel_req2_test3() + + exp_scp_result = call_result.SetChargingProfilePayload( + ChargingProfileStatus.accepted + ) + + assert await charge_point_v16.set_charging_profile_req(req1) == exp_scp_result + assert await charge_point_v16.set_charging_profile_req(req2) == exp_scp_result + + # start charging session + test_controller.plug_in() + + # send RemoteStartTransaction.req + await charge_point_v16.remote_start_transaction_req( + id_tag=test_config.authorization_info.valid_id_tag_1, connector_id=1 + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "RemoteStartTransaction", + call_result.RemoteStartTransactionPayload(RemoteStartStopStatus.accepted), + validate_remote_start_stop_transaction, + ) + + # expect StartTransaction.req + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StartTransaction", + call.StartTransactionPayload( + 1, test_config.authorization_info.valid_id_tag_1, 0, "" + ), + validate_standard_start_transaction, + ) + + await charge_point_v16.get_composite_schedule( + call.GetCompositeSchedulePayload(connector_id=1, duration=90) + ) + + exp = rel_exp_test3() + + # expect correct GetCompositeSchedule.conf + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetCompositeSchedule", + exp, + validate_composite_schedule, + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/authorization.py b/tests/ocpp_tests/test_sets/ocpp201/authorization.py new file mode 100644 index 000000000..636adacd8 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/authorization.py @@ -0,0 +1,1024 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +# fmt: off +import pytest +from datetime import datetime +import logging + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +from validations import validate_status_notification_201 +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * + +from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from ocpp.v201.enums import (Action, IdTokenType as IdTokenTypeEnum, SetVariableStatusType, ClearCacheStatusType, ConnectorStatusType,GetVariableStatusType) +from ocpp.v201.datatypes import * +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 +from ocpp.routing import on, create_route_map +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 + +# fmt: on + +log = logging.getLogger("authorizationTest") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_authorize_01( + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + test_controller.swipe("DEADBEEF") + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload( + id_token=IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443) + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_c09( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + C09.FR.03 + C09.FR.04 + C09.FR.05 + C09.FR.07 + C09.FR.09 + C09.FR.10 + C09.FR.11 + C09.FR.12 + """ + + # Enable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set MasterPassGroupId + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "MasterPassGroupId", "00000000" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set AuthCacheLifeTime + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "LifeTime", "86400" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Clear cache + r: call_result201.ClearCachePayload = await charge_point_v201.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + + accepted_tags = ["001", "002"] + + def get_token_info(token: str): + if token in accepted_tags: + return IdTokenInfoType( + status=AuthorizationStatusType.accepted, + group_id_token=IdTokenType( + id_token="123", type=IdTokenTypeEnum.central + ), + ) + else: + return IdTokenInfoType( + status=AuthorizationStatusType.blocked, + group_id_token=IdTokenType( + id_token="123", type=IdTokenTypeEnum.central + ), + ) + + @on(Action.Authorize) + def on_authorize(**kwargs): + msg = call201.AuthorizePayload(**kwargs) + msg_token = IdTokenType(**msg.id_token) + return call_result201.AuthorizePayload( + id_token_info=get_token_info(msg_token.id_token) + ) + + @on(Action.TransactionEvent) + def on_transaction_event(**kwargs): + msg = call201.TransactionEventPayload(**kwargs) + if msg.id_token != None: + msg_token = IdTokenType(**msg.id_token) + return call_result201.TransactionEventPayload( + id_token_info=get_token_info(msg_token.id_token) + ) + else: + return call_result201.TransactionEventPayload() + + setattr(charge_point_v201, "on_authorize", on_authorize) + setattr(charge_point_v201, "on_transaction_event", on_transaction_event) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + # Wait for ready and make sure all messages are read into the test_utility + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 2}, + ) + test_utility.messages.clear() + + test_controller.swipe("001") + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload( + id_token=IdTokenType(id_token="001", type=IdTokenTypeEnum.iso14443) + ), + ) + + test_controller.plug_in() + # eventType=Started + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + test_controller.swipe("002") + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload( + id_token=IdTokenType(id_token="002", type=IdTokenTypeEnum.iso14443) + ), + ) + + # eventType=Ended + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Ended", + "triggerReason": "StopAuthorized", + "transactionInfo": {"stoppedReason": "Local"}, + }, + ) + + test_controller.plug_out() + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + test_utility.messages.clear() + + test_controller.swipe("001") + test_controller.plug_in() + # eventType=Started + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + # C09.FR.07: With a valid token in cache with the same groupId the CS shall end + # the autorization of the transaction without first sending an AuthorizeRequest + test_utility.forbidden_actions.append("Authorize") + + test_controller.swipe("002") + # eventType=Ended + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Ended", + "triggerReason": "StopAuthorized", + "transactionInfo": {"stoppedReason": "Local"}, + }, + ) + + test_controller.plug_out() + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + test_utility.messages.clear() + + # Allow Authorize message again + test_utility.forbidden_actions.remove("Authorize") + + test_controller.swipe("001") + test_controller.plug_in() + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + # C09.FR.11: Swipe card with groupIdToken the same as transacton but status blocked SHALL NOT stop the transaction + # Instead the plug out should stop the transaction. The transactionEvent will tell us which one it was. + test_controller.swipe("003") + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Ended", + "triggerReason": "EVCommunicationLost", + "transactionInfo": {"stoppedReason": "EVDisconnected"}, + }, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_c10_c11_c12( + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + + # prepare data for the test + evse_id = 1 + connector_id = 1 + + # Enable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set AuthCacheLifeTime + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "LifeTime", "86400" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Clear cache + r: call_result201.ClearCachePayload = await charge_point_v201.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + + test_controller.swipe("DEADBEEF") + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload( + id_token=IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443) + ), + ) + + test_controller.plug_in() + # eventType=Started + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + test_utility.messages.clear() + test_controller.plug_out() + # eventType=Ended + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + test_utility.messages.clear() + + await asyncio.sleep(2) + + # because LocalPreAuthorize is true we dont expect an Authorize.req this time + test_utility.forbidden_actions.append("Authorize") + + test_controller.swipe("DEADBEEF") + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.occupied, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + # because LocalPreAuthorize is true we dont expect an authorize here + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started"}, + ) + ) + + # Disable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set AuthCacheLifeTime to 1s + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "LifeTime", "1" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + test_utility.messages.clear() + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + # eventType=Ended + await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + test_utility.forbidden_actions.clear() + + test_controller.swipe("DEADBEEF") + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload( + id_token=IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443) + ), + ) + + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + test_controller.plug_in() + # eventType=Started + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + test_utility.messages.clear() + test_controller.plug_out() + # eventType=Ended + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_c15( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + C15.FR.01 + C15.FR.02 + C15.FR.03 + C15.FR.04 + C15.FR.05 + C15.FR.06 + C15.FR.07 + C15.FR.08 + """ + log.info( + " ########### Test case C15: Offline Authorization of unknown Id ###########" + ) + + # prepare data for the test + evse_id = 1 + connector_id = 1 + + # make an unknown IdToken + id_tokenC15 = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443) + + # Generate a transaction response + # TODO: This needs to be adapted for C15.FR.03-07 use cases + @on(Action.TransactionEvent) + def on_transaction_event(**kwargs): + msg = call201.TransactionEventPayload(**kwargs) + if msg.id_token != None: + if stop_tx_on_invalid_id != None: + return call_result201.TransactionEventPayload( + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.unknown + ) + ) + else: + return call_result201.TransactionEventPayload( + id_token_info=IdTokenInfoType( + status=AuthorizationStatusType.accepted + ) + ) + else: + return call_result201.TransactionEventPayload() + + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + setattr(charge_point_v201, "on_transaction_event", on_transaction_event) + + central_system_v201.function_overrides.append( + ("on_transaction_event", on_transaction_event) + ) + + # Enable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set AuthCacheLifeTime + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "LifeTime", "86400" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Clear cache + r: call_result201.ClearCachePayload = await charge_point_v201.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + + # set AuthorizeRemoteStart to false + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "AuthorizeRemoteStart", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # accept all transaction requests for now + stop_tx_on_invalid_id = None + + # Get the value of MaxEnergyOnInvalidId + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "TxCtrlr", "MaxEnergyOnInvalidId" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + if ( + get_variables_result.attribute_status + == GetVariableStatusType.not_supported_attribute_type + ): + pass + else: + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "TxCtrlr", "MaxEnergyOnInvalidId", "0" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + log.debug( + "==============================================C15.FR.08 ====================================" + ) + log.debug( + "The Charging Station rejects the unknown IdToken if OfflineTxForUnknownIdEnabled is set False " + ) + # Disable offline authorization for unknown ID + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + test_utility.messages.clear() + + # Disconnect CS + log.debug(" Disconnect the CS from the CSMS") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # because offline authorization for unknown id is false, it shouldn't allow a transaction + test_utility.forbidden_actions.append("TransactionEvent") + + # start charging session + test_controller.plug_in() + + # swipe id tag to authorize + log.debug("Attempt to Authorize") + test_controller.swipe(id_tokenC15.id_token) + + await asyncio.sleep(3) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + test_controller.plug_out() + + # TODO: Currently fails here because WS doesnt recognize its disconnected and still sends the Authorize.req + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + test_utility.messages.clear() + test_utility.forbidden_actions.clear() + + # C15.FR.08 + log.debug( + "==============================================C15.FR.08 ====================================" + ) + log.debug( + "The Charging Station accepts the unknown IdToken if OfflineTxForUnknownIdEnabled is set True " + ) + # Enable offline authorization for unknown ID + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Disconnect CS + log.debug(" Disconnect the CS from the CSMS") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe id tag to authorize + test_controller.swipe(id_tokenC15.id_token) + + # start charging session + test_controller.plug_in() + + await asyncio.sleep(2) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + log.debug( + "==============================================C15.FR.02 ====================================" + ) + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + # swipe id tag to finish transaction + test_controller.swipe(id_tokenC15.id_token) + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + # unplug + test_controller.plug_out() + + test_utility.messages.clear() + test_utility.forbidden_actions.clear() + + # # C15.FR.03. Commented because preconditions are unmet + # The transaction is still ongoing AND StopTxOnInvalidId is true AND TxStopPoint does NOT contain: (Authorized OR PowerPathClosed OR EnergyTransfer) + # log.debug("=================================================C15.FR.03 ======================================================") + # # Enable stop Tx on invalid Id + # r: call_result201.SetVariablesPayload = await charge_point_v201.set_config_variables_req("TxCtrlr","StopTxOnInvalidId","true") + # set_variable_result: SetVariableResultType = SetVariableResultType(**r.set_variable_result[0]) + # assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # # Get the value of StopTxOnInvalidId + # r: call_result201.GetVariablesPayload = await charge_point_v201.get_config_variables_req("TxCtrlr","StopTxOnInvalidId") + # get_variables_result: GetVariableResultType = GetVariableResultType(**r.get_variable_result[0]) + # assert get_variables_result.attribute_status == GetVariableStatusType.accepted + # stop_tx_on_invalid_id = json.loads(get_variables_result.attribute_value) + + # # Get the value of TxStopPoint + # r: call_result201.GetVariablesPayload = await charge_point_v201.get_config_variables_req("TxCtrlr","TxStopPoint") + # get_variables_result: GetVariableResultType = GetVariableResultType(**r.get_variable_result[0]) + # assert get_variables_result.attribute_status == GetVariableStatusType.accepted + + # # tx_stop_point = json.loads(get_variables_result.attribute_value) + # # log.debug(" TxStop Point: %s " %(tx_stop_point)) + + # # Disconnect CS + # log.debug(" Disconnect the CS from the CSMS") + # test_controller.disconnect_websocket() + + # await asyncio.sleep(2) + + # # swipe id tag to authorize + # test_controller.swipe(id_tokenC15.id_token) + + # # start charging session + # test_controller.plug_in() + + # await asyncio.sleep(2) + + # # Connect CS + # log.debug(" Connect the CS to the CSMS") + # test_controller.connect_websocket() + + # #wait for reconnect + # charge_point_v201 = await central_system_v201.wait_for_chargepoint(wait_for_bootnotification=False) + + # # should send a Transaction event C15.FR.02 + # assert await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"}) + + # # should send a Transaction event C15.FR.04 with ended + # assert await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent",{ + # "eventType": "Updated", + # "triggerReason": "Deauthorized", + # "transactionInfo": { + # "chargingState": "SuspendedEVSE"}}) + + # # unplug + # test_controller.plug_out() + + # test_utility.messages.clear() + # test_utility.forbidden_actions.clear() + + # C15.FR.04 if Transaction event response is not accepted and transaction is ongoing + log.debug( + "=================================================C15.FR.04 ======================================================" + ) + # Enable stop Tx on invalid Id + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "TxCtrlr", "StopTxOnInvalidId", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Get the value of StopTxOnInvalidId + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req("TxCtrlr", "StopTxOnInvalidId") + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + stop_tx_on_invalid_id = json.loads(get_variables_result.attribute_value) + + # # Get the value of TxStopPoint + # r: call_result201.GetVariablesPayload = await charge_point_v201.get_config_variables_req("TxCtrlr","TxStopPoint") + # get_variables_result: GetVariableResultType = GetVariableResultType(**r.get_variable_result[0]) + # assert get_variables_result.attribute_status == GetVariableStatusType.accepted + + # tx_stop_point = json.loads(get_variables_result.attribute_value) + # log.debug(" TxStop Point: %s " %(tx_stop_point)) + + # Disconnect CS + log.debug(" Disconnect the CS from the CSMS") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe id tag to authorize + test_controller.swipe(id_tokenC15.id_token) + + # start charging session + test_controller.plug_in() + + await asyncio.sleep(2) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + # should send a Transaction event C15.FR.04 with ended + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Ended", + "triggerReason": "Deauthorized", + "transactionInfo": {"stoppedReason": "DeAuthorized"}, + }, + ) + + # #C15.FR.05 The cable should be locked until the user presents the token. + # Commented beacuse cable currently cannot be locked in place + # log.debug("==============================C15.FR.05=====================================") + # test_controller.plug_out() + + # #connector status should still be occupied + # assert await wait_for_and_validate(test_utility, charge_point_v201, "StatusNotification", + # call201.StatusNotificationPayload(datetime.now().isoformat(), + # ConnectorStatusType.occupied, evse_id, connector_id), + # validate_status_notification_201) + + # # swipe id tag to authorize + # test_controller.swipe(id_tokenC15.id_token) + + # #connector status should still be available + # assert await wait_for_and_validate(test_utility, charge_point_v201, "StatusNotification", + # call201.StatusNotificationPayload(datetime.now().isoformat(), + # ConnectorStatusType.available, evse_id, connector_id), + # validate_status_notification_201) + + # C15.FR.05 The cable should be locked until the user presents the token + test_controller.plug_out() + + test_utility.messages.clear() + test_utility.forbidden_actions.clear() + + # C15.FR.06 + log.debug( + "==============================================C15.FR.06 ====================================" + ) + + # Disable stop Tx on invalid Id + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "TxCtrlr", "StopTxOnInvalidId", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Get the value of StopTxOnInvalidId + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req("TxCtrlr", "StopTxOnInvalidId") + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + stop_tx_on_invalid_id = json.loads(get_variables_result.attribute_value) + + # Disconnect CS + log.debug("Disconnect the CS from the CSMS") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe id tag to authorize + test_controller.swipe(id_tokenC15.id_token) + + # start charging session + test_controller.plug_in() + + # TODO: This should work with smaller values too. Currently there is an issue when stopped in PrepareCharging state. + await asyncio.sleep(10) + + # Connect CS + log.debug("Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "ChargingStateChanged", + "transactionInfo": {"chargingState": "SuspendedEVSE"}, + }, + ) + + # swipe id tag to finish transaction + test_controller.swipe(id_tokenC15.id_token) + + # unplug + test_controller.plug_out() + + test_utility.messages.clear() + test_utility.forbidden_actions.clear() + + # #C15.FR.06 + # Commented because MaxEnergyOnInvalidId isn't implemented + # log.debug("==============================================C15.FR.07 ====================================") + + # #Disable stop Tx on invalid Id + # r: call_result201.SetVariablesPayload = await charge_point_v201.set_config_variables_req("TxCtrlr","StopTxOnInvalidId","false") + # set_variable_result: SetVariableResultType = SetVariableResultType(**r.set_variable_result[0]) + # assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # # Get the value of StopTxOnInvalidId + # r: call_result201.GetVariablesPayload = await charge_point_v201.get_config_variables_req("TxCtrlr","StopTxOnInvalidId") + # get_variables_result: GetVariableResultType = GetVariableResultType(**r.get_variable_result[0]) + # assert get_variables_result.attribute_status == GetVariableStatusType.accepted + # stop_tx_on_invalid_id = json.loads(get_variables_result.attribute_value) + + # #Set a value for MaxEnergyOnInvalidId + # r: call_result201.SetVariablesPayload = await charge_point_v201.set_config_variables_req("TxCtrlr","MaxEnergyOnInvalidId","16") + # set_variable_result: SetVariableResultType = SetVariableResultType(**r.set_variable_result[0]) + # assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # # Get the value of MaxEnergyOnInvalidId + # r: call_result201.GetVariablesPayload = await charge_point_v201.get_config_variables_req("TxCtrlr","MaxEnergyOnInvalidId") + # get_variables_result: GetVariableResultType = GetVariableResultType(**r.get_variable_result[0]) + # if get_variables_result.attribute_status == GetVariableStatusType.accepted: + # max_energy_on_invalid_id = json.loads(get_variables_result.attribute_value) + # log.debug("max energy on invalid Id %s " %max_energy_on_invalid_id) + # else: + # max_energy_on_invalid_id = None + + # # Disconnect CS + # log.debug(" Disconnect the CS from the CSMS") + # test_controller.disconnect_websocket() + + # await asyncio.sleep(2) + + # # swipe id tag to authorize + # test_controller.swipe(id_tokenC15.id_token) + + # # start charging session + # test_controller.plug_in() + + # await asyncio.sleep(2) + + # # Connect CS + # log.debug(" Connect the CS to the CSMS") + # test_controller.connect_websocket() + + # #wait for reconnect + # charge_point_v201 = await central_system_v201.wait_for_chargepoint(wait_for_bootnotification=False) + + # # should send a Transaction event C15.FR.02 + # assert await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"}) + + # swipe id tag to finish transaction + test_controller.swipe(id_tokenC15.id_token) + + # unplug + test_controller.plug_out() diff --git a/tests/ocpp_tests/test_sets/ocpp201/availability.py b/tests/ocpp_tests/test_sets/ocpp201/availability.py new file mode 100644 index 000000000..519fd3933 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/availability.py @@ -0,0 +1,409 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) + +from ocpp.v201.enums import OperationalStatusType, ChangeAvailabilityStatusType +from ocpp.v201.datatypes import EVSEType +from ocpp.v201 import call_result as call_result + +# fmt: off +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest_test_utils import * +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +# fmt: on + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-ocpp201.yaml") +) +async def test_g03( + central_system_v201: CentralSystem, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + evse_1 = EVSEType(id=1) + evse_1_1 = EVSEType(id=1, connector_id=1) + evse_2 = EVSEType(id=2) + evse_2_1 = EVSEType(id=2, connector_id=1) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_1_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_2_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + test_utility.forbidden_actions.append("StatusNotification") + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_2 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_2 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + test_utility.forbidden_actions.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_1_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + + test_utility.messages.clear() + + test_controller.stop() + await asyncio.sleep(1) + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_2_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Available"}, + ) + + test_controller.swipe("001", connectors=[1]) + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "evse": {"id": 1}}, + ) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_1_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.scheduled + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_2_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.scheduled + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_2 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + test_utility.messages.clear() + + # try state that EVSE is already in + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse_1_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse_1_1 + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config( + get_everest_config_path_str("everest-config-ocpp201.yaml") +) +async def test_g04( + central_system_v201: CentralSystem, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + test_controller.swipe("001", connectors=[1]) + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "evse": {"id": 1}}, + ) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative + ) + ) + assert r.status == ChangeAvailabilityStatusType.scheduled + + test_controller.plug_out() + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + test_utility.messages.clear() + + test_controller.stop() + await asyncio.sleep(1) + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"}, + ) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 2, "connectorId": 1, "connectorStatus": "Available"}, + ) + + await asyncio.sleep(2) + + test_utility.messages.clear() + + test_controller.swipe("001", connectors=[1]) + + await asyncio.sleep(2) + + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "evse": {"id": 1}}, + ) + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative + ) + ) + assert r.status == ChangeAvailabilityStatusType.scheduled + + test_utility.messages.clear() + + r: call_result.ChangeAvailabilityPayload = ( + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative + ) + ) + assert r.status == ChangeAvailabilityStatusType.accepted + + await asyncio.sleep(2) + + test_controller.plug_out() + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/data_transfer.py b/tests/ocpp_tests/test_sets/ocpp201/data_transfer.py new file mode 100644 index 000000000..ddb3b336d --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/data_transfer.py @@ -0,0 +1,317 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest_asyncio + +# fmt: off +import logging +from copy import deepcopy +from typing import Dict +from unittest.mock import Mock, call as mock_call +import json +import time +import pytest + +from everest.testing.core_utils.common import Requirement +from everest.testing.ocpp_utils.central_system import CentralSystem + +from test_sets.everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +from everest.testing.core_utils.probe_module import ProbeModule +from everest.testing.core_utils import EverestConfigAdjustmentStrategy + +log = logging.getLogger("ocpp201DataTransferTest") + +async def await_mock_called(mock): + while not mock.call_count: + await asyncio.sleep(0.1) + +# FIXME: redefine probe_module and chargepoint_with_pm here until the ones in conftest.py are fixed + +@pytest.fixture +def probe_module(started_test_controller, everest_core) -> ProbeModule: + # initiate the probe module, connecting to the same runtime session the test controller started + module = ProbeModule(everest_core.get_runtime_session()) + + return module + +@pytest_asyncio.fixture +async def chargepoint_with_pm(central_system: CentralSystem, probe_module: ProbeModule): + """Fixture for ChargePoint16. Requires central_system_v201 and test_controller. Starts test_controller immediately + """ + probe_module.start() + await probe_module.wait_to_be_ready() + + # wait for libocpp to go online + cp = await central_system.wait_for_chargepoint() + yield cp + await cp.stop() + +class ProbeModuleDataTransferConfigurationAdjustment(EverestConfigAdjustmentStrategy): + def adjust_everest_configuration(self, everest_config: Dict): + adjusted_config = deepcopy(everest_config) + + adjusted_config["active_modules"]["ocpp"]["connections"]["data_transfer"] = [{"module_id": "probe", "implementation_id": "ProbeModuleDataTransfer"}] + + return adjusted_config + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config("everest-config-ocpp201-data-transfer.yaml") +@pytest.mark.inject_csms_mock +class TestOcpp201DataTransferIntegration: + """ + Integration tests for the OCPP201 Module's implementation of the P-test cases (data transfer) + Uses the probe module and a mock CSMS. + """ + + @pytest.mark.parametrize("response_status", + ["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"], + ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"]) + @pytest.mark.parametrize("message_id", + ["message123", None], + ids=["with_msg_id", "no_msg_id"]) + @pytest.mark.parametrize("data", + ["string_data", 42, 1.2345, False, None], + ids=["string_data", "int_data", "float_data", "bool_data", "no_data"]) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_p1(self, response_status, message_id, data, central_system: CentralSystem, probe_module): + """ + Use case P01: Data transfer to the Charging Station + """ + probe_module_mock_fn = Mock() + probe_module_mock_fn.side_effect = [{ + "status": response_status, + "data": json.dumps("response123") + }] + probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + # wait for libocpp to go online + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + data_transfer_result: call_result201.DataTransferPayload = await chargepoint_with_pm.data_transfer_req( + message_id=message_id, + data=data, + vendor_id="vendor123" + ) + + assert data_transfer_result == call_result201.DataTransferPayload( + data="response123", + status=response_status + ) + if message_id is None: + if data is None: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "vendor_id": "vendor123" + } + })] + else: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "vendor_id": "vendor123", + "data": json.dumps(data) + } + })] + else: + if data is None: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "message_id": message_id, + "vendor_id": "vendor123" + } + })] + else: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "message_id": message_id, + "vendor_id": "vendor123", + "data": json.dumps(data) + } + })] + + @pytest.mark.parametrize("response_status", + ["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"], + ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"]) + @pytest.mark.parametrize("message_id", + ["message987", None], + ids=["with_msg_id", "no_msg_id"]) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_p1_json(self, response_status, message_id, central_system: CentralSystem, probe_module): + """ + Use case P01: Data transfer to the Charging Station + """ + probe_module_mock_fn = Mock() + probe_module_mock_fn.side_effect = [{ + "status": response_status, + "data": "{\"response987\":\"hello\"}" + }] + probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + # wait for libocpp to go online + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + data_transfer_result: call_result201.DataTransferPayload = await chargepoint_with_pm.data_transfer_req( + message_id=message_id, + data={"request987":"hi"}, + vendor_id="vendor123" + ) + + assert data_transfer_result == call_result201.DataTransferPayload( + data={'response987':'hello'}, + status=response_status + ) + if message_id is None: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "vendor_id": "vendor123", + "data": "{\"request987\":\"hi\"}" + } + })] + else: + assert probe_module_mock_fn.mock_calls == [mock_call({ + "request": { + "message_id": message_id, + "vendor_id": "vendor123", + "data": "{\"request987\":\"hi\"}" + } + })] + + @pytest.mark.parametrize("response_status", + ["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"], + ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"]) + @pytest.mark.parametrize("message_id", + ["message123", None], + ids=["with_msg_id", "no_msg_id"]) + @pytest.mark.parametrize("data", + ["string_data", 42, 1.2345, False, None], + ids=["string_data", "int_data", "float_data", "bool_data", "no_data"]) + @pytest.mark.probe_module( + connections={ + "ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")] + } + ) + @pytest.mark.asyncio + async def test_p2(self, response_status, message_id, data, central_system: CentralSystem, + chargepoint_with_pm: ChargePoint201, probe_module): + """ + Use case P02: Data transfer to the CSMS + """ + central_system.mock.on_data_transfer.side_effect = [ + call_result201.DataTransferPayload(status=response_status, data="response123") + ] + + response = json.dumps("response123") + if message_id is None: + if data is None: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123"} + }) == {"status": response_status, "data": response} + else: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123", "data": json.dumps(data)} + }) == {"status": response_status, "data": response} + else: + if data is None: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123", "message_id": message_id} + }) == {"status": response_status, "data": response} + else: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123", "message_id": message_id, "data": json.dumps(data)} + }) == {"status": response_status, "data": response} + + @pytest.mark.parametrize("response_status", + ["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"], + ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"]) + @pytest.mark.parametrize("message_id", + ["message987", None], + ids=["with_msg_id", "no_msg_id"]) + @pytest.mark.probe_module( + connections={ + "ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")] + } + ) + @pytest.mark.asyncio + async def test_p2_json(self, response_status, message_id, central_system: CentralSystem, + chargepoint_with_pm: ChargePoint201, probe_module): + """ + Use case P02: Data transfer to the CSMS + """ + central_system.mock.on_data_transfer.side_effect = [ + call_result201.DataTransferPayload(status=response_status, data={'response987':'hello'}) + ] + + if message_id is None: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123", "data": "{\"request987\":\"hi\"}"} + }) == {"status": response_status, "data": "{\"response987\":\"hello\"}"} + else: + assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", { + "request": {"vendor_id": "vendor123", "message_id": message_id, "data": "{\"request987\":\"hi\"}"} + }) == {"status": response_status, "data": "{\"response987\":\"hello\"}"} + + @pytest.mark.asyncio + async def test_p1_no_callback(self, charge_point: ChargePoint201): + """ + Use case P01: Data transfer to the Charging Station + """ + data_transfer_result: call_result201.DataTransferPayload = await charge_point.data_transfer_req( + message_id="message123", + data="request123", + vendor_id="vendor123" + ) + + assert data_transfer_result == call_result201.DataTransferPayload( + data=None, + status="UnknownVendorId" + ) + + @pytest.mark.probe_module( + connections={ + "ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")] + } + ) + + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment()) + @pytest.mark.asyncio + @pytest.mark.skip("Fails because callback sleeps for 400s and test case expects response. Check expected behavior") + async def test_p1_no_response(self, central_system: CentralSystem, probe_module): + """ + Use case P01: Data transfer to the Charging Station but Charging Station does not respond + """ + + def data_transfer_side_effect(*args, **kwargs): + time.sleep(400) + return call_result201.DataTransferPayload(status="Accepted", data={'response987':'hello'}) + + probe_module_mock_fn = Mock() + probe_module_mock_fn.side_effect = data_transfer_side_effect + probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + # wait for libocpp to go online + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + data_transfer_result: call_result201.DataTransferPayload = await chargepoint_with_pm.data_transfer_req( + message_id="message123", + data="data", + vendor_id="vendor123" + ) + + assert data_transfer_result == call_result201.DataTransferPayload( + status="Rejected" + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/iso15118_certificate_management.py b/tests/ocpp_tests/test_sets/ocpp201/iso15118_certificate_management.py new file mode 100644 index 000000000..bba8c593a --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/iso15118_certificate_management.py @@ -0,0 +1,1064 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import copy +import logging +import pytest +from dataclasses import field, dataclass +from typing import List, Dict +from unittest.mock import Mock, call as mock_call + +from everest.testing.ocpp_utils.central_system import CentralSystem + +from ocpp.v201.enums import GetInstalledCertificateStatusType, GetCertificateIdUseType + +from ocpp.v201 import call as call201 + +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP201ConfigAdjustment, +) + +from test_sets.everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from everest.testing.ocpp_utils.charge_point_utils import ( + wait_for_and_validate, + TestUtility, +) +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +from everest.testing.core_utils.probe_module import ProbeModule + +log = logging.getLogger("iso15118CertificateManagementTest") + + +async def await_mock_called(mock): + while not mock.call_count: + await asyncio.sleep(0.1) + + +@pytest.fixture() +def example_certificate(): + certificate = """-----BEGIN CERTIFICATE----- +MIIFlzCCA3+gAwIBAgIUVMBWzWyLetKgv4+kDH19eo/GM6MwDQYJKoZIhvcNAQEL +BQAwWjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxDzANBgNVBAoMBlBp +b25peDEMMAoGA1UECwwDREVWMRowGAYDVQQDDBFUZXN0IENTTVMgUm9vdCBDQTAg +Fw0yMzEwMjMxMTQzNDJaGA80NzYxMDkxODExNDM0MlowWjELMAkGA1UEBhMCREUx +EDAOBgNVBAgMB0dlcm1hbnkxDzANBgNVBAoMBlBpb25peDEMMAoGA1UECwwDREVW +MRowGAYDVQQDDBFUZXN0IENTTVMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAK0kxp3gaNU4RfhwQVA2/fGV8s1O0j6NWVOmJjidGnsghbTO +mXe8gIbCdTraMFejpofBt9X5UFm5FDRAeVF3QhgRQ4m5AzecwdWI737Lst3+FL++ +ydx0I5rBrwM53p/mYKiX+bRTv0MjGmRrB2+HjUwvwNjanvdk/RTsclEXwFPo4LQd +NqOdrBGcL7KYAh+OtJLbRc9dxy18KA0KnbanrPdNh6wdRRPd4G3KdkNvLXT2PNy1 +KxZgcIXHhc5jSrcBpTV/yWXWk96Sdy/yQprwF0GfMKJcEe4J6lea4l8gpiGhOGp4 +s6CI0KucfTMd8qfTuIP+Rh62wkIP8psPhthJq6r/xA7wqHJ+Ae1w0qJWD6cTCzM/ +l5eoPE8zI4vK04S2T9AR8o7CjPrhGQMa7z1+tn+uBoh5qIz27NJz5xCpTOA94l80 +NHRlEJprEydk9YrecGi5SSBLf31OBLBycptLc2uXj4sPqzHFC0z1YG+5Nd8tHDIN +qcBepE+KgFwc0KKwgm1gtl2/s5SVBNSdM6h3dbol18r+B+29Up4F88o/DXH1OO9Y +eZuWGCltn6lhSMH+pmXTskI78o2RDFMAyndaGN0YpV9AZvkIdG2ps1Fs/VdD59P5 +8fw4xh9lW2NDqK5uY+tY49aCYcA9hgiNXyEZcdZVY9que41cGeTobH1YA+rPAgMB +AAGjUzBRMB0GA1UdDgQWBBToGGVj0VQC/ZXxcdFaIgXqSp6xeDAfBgNVHSMEGDAW +gBToGGVj0VQC/ZXxcdFaIgXqSp6xeDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQCn7ELupRtTEuLIzC5fb9+bzhURBUosNsauqPIkcEZ8d33bQZGU +E5xKTlRcsKoQJ9+TK6THWZ6cXBW90lhe6db6rR0/tYLJzhGBryAjaX071Mphalzx +9gsQ3flKEwDAnpcfyRY7AZMapEzFwuXoBY1qN1gMLVCQQUGgVBbKQ8vctfZJ7EXI +Uj45ZCSp8nfoiKILFhVs3jSHknxckscFgLkb9P/nOdp24kEx9vMfj5tXuituLHFt +eIuv83dULl5tZSBS5LxQyXjaJNWJIH+Bm9zxxZQqmrwo9JFDyUYVFz2T58mJz6Fb +kS6zMhO+P/7tV4sSSnBwF7E0uh3rYmDOcBPyUvxb44WWOQ8G1Jux+X/HI6paWvFy +CZTRl/lgUYbQRURq4w7HAnHlCVvfJzPT+sr3Ruithg+jQC7BaV2zCpKUksnK/3ln +VGZaRD6xwGGXUBAxDbjXkZvMnkGr6Iu1L6OEPF97sKSrmRMd8hn9RLKPxXDiLUzC +VD5nwEkO5Poai0b4MB9K1YNtMxc17k3EBOIGPswfp0QQPPTTy2xwP2WrFU65P1G3 +Zq7pg1dChb1JX1IhdJbIlwtlkA0+ZpuFAE8q84zuoTxPyi3S0DsCjAkmYBstb6wK +CAnDdUF7Zy+eXIqWUHmXHSk4hcEiAUYx8enMUPgjE8VpcPqXzxxS+Nt2Ig== +-----END CERTIFICATE-----""" + + cert_hash_data = { + "issuer_key_hash": "0b89ba5d6aebd520cb686d75911c3b1e236ef3a5137e298e2395e97bad049a9c", # pkcs1, + # "issuer_key_hash": 'a6e29e28f8f019381f712fbd19792a4247812f7faccc7ef73db78adb8ee59132', # default + "issuer_name_hash": "608964fb2fa9b01051979832e94b5dfc69f41dc76e94321bff1489220f0edd51", + "serial_number": "54c056cd6c8b7ad2a0bf8fa40c7d7d7a8fc633a3", + "hash_algorithm": "SHA256", + } + + return {"certificate": certificate, "certificate_hash_data": cert_hash_data} + + +@dataclass(frozen=True) +class CertificateHashData: + issuer_key_hash: str + issuer_name_hash: str + serial_number: str + hash_algorithm: str = "SHA256" + + def __repr__(self): + return f"CertificateHashData(Serial: {self.serial_number} Issuer: {self.issuer_key_hash[:6]}... / {self.issuer_name_hash[:6]}... )" + + +@dataclass(frozen=True, unsafe_hash=True) +class CertificateHashDataChainEntry: + certificate_type: str + certificate_hash_data: CertificateHashData + child_certificate_hash_data: List[CertificateHashData] = field(hash=False) + + @staticmethod + def from_dict(data: Dict): + return CertificateHashDataChainEntry( + certificate_type=data["certificate_type"], + certificate_hash_data=CertificateHashData(**data["certificate_hash_data"]), + child_certificate_hash_data=[ + CertificateHashData(**d) + for d in data.get("child_certificate_hash_data", []) + ], + ) + + def __repr__(self): + s = f"( {self.certificate_type}: {self.certificate_hash_data}" + if self.child_certificate_hash_data: + s += "\n children: \n" + s += "\n".join(f"\t\t{c}" for c in self.child_certificate_hash_data) + s += "\n" + s += ")" + return s + + +@dataclass(frozen=True) +class CertificateHashDataChain: + entries: List[CertificateHashDataChainEntry] + + @staticmethod + def from_list(data: list[dict]): + return CertificateHashDataChain( + entries=[CertificateHashDataChainEntry.from_dict(d) for d in data] + ) + + def __eq__(self, other): + return set(self.entries) == set(other.entries) + + def __repr__(self): + return ( + "CertificateHashDataChain(\n" + + "\n".join(f"\t{d}" for d in self.entries) + + "\n)" + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config("everest-config-ocpp201-probe-module.yaml") +@pytest.mark.inject_csms_mock +@pytest.mark.probe_module +class TestIso15118CertificateManagementOcppIntegration: + """ """ + + # ************************************************************************************************ + # Use Case M1-M2: EV sends install or update Certificate request + # ************************************************************************************************ + + @pytest.mark.parametrize( + "skip_implementation", + [ + { + "ProbeModuleConnectorA": ["set_get_certificate_response"], + "ProbeModuleConnectorB": ["set_get_certificate_response"], + } + ], + ) + @pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "ISO15118Ctrlr", + "ContractCertificateInstallationEnabled", + "Actual", + ), + True, + ) + ] + ) + ) + @pytest.mark.parametrize( + "response_status", ["Accepted", "Failed"], ids=["successful", "failed"] + ) + @pytest.mark.parametrize( + "action", + ["Install", "Update"], + ids=["M01 - Certificate installation", "M02 - Certificate Update EV"], + ) + async def test_m1_m2_certificate_request_ev( + self, + action, + response_status, + central_system: CentralSystem, + probe_module, + test_utility: TestUtility, + ): + """ + Tests Error handling of M01 and M02 + Tested requirements: M01.FR.01, MR02.FR.01 + """ + connectors = ["ProbeModuleConnectorA", "ProbeModuleConnectorB"] + + mock_cmd_set_get_certificate_response = {} + for connector_id in connectors: + mock_cmd_set_get_certificate_response[connector_id] = Mock() + mock_cmd_set_get_certificate_response[connector_id].return_value = None + probe_module.implement_command( + connector_id, + "set_get_certificate_response", + mock_cmd_set_get_certificate_response[connector_id], + ) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Each connector sends an installation request + for connector_index, calling_connector_id in enumerate(connectors): + + # Setup ChargePoint response + + exi_response = f"mock exi response for {calling_connector_id}" + + central_system.mock.on_get_15118_ev_certificate.side_effect = [ + call_result201.Get15118EVCertificatePayload( + status=response_status, exi_response=exi_response + ) + ] + + # Act: Publish Install Certificate requeset + mock_certificate_installation_req = base64.b64encode( + f"{calling_connector_id} mock Raw CertificateInstallationReq or CertificateUpdateReq message as exi stream".encode( + "utf-8" + ) + ).decode("utf-8") + + mock_iso15118_schema_version = f"{calling_connector_id} mock Schema Version" + + probe_module.publish_variable( + calling_connector_id, + "iso15118_certificate_request", + { + "exi_request": mock_certificate_installation_req, + "iso15118_schema_version": mock_iso15118_schema_version, + "certificate_action": action, + }, + ) + + # Verify: CSMS is called correctly + expected_cp_request = call201.Get15118EVCertificatePayload( + iso15118_schema_version=mock_iso15118_schema_version, + exi_request=mock_certificate_installation_req, + action=action, + ) + + assert await wait_for_and_validate( + test_utility, + chargepoint_with_pm, + exp_action="Get15118EVCertificate", + exp_payload=expected_cp_request, + ) + + # Verify: Certificate response forwarded to correct EVSE manager as commmand + called_mock = mock_cmd_set_get_certificate_response[calling_connector_id] + other_connector_id = connectors[(connector_index + 1) % len(connectors)] + uncalled_mock = mock_cmd_set_get_certificate_response[other_connector_id] + + await asyncio.wait_for(await_mock_called(called_mock), 3) + + assert called_mock.mock_calls == [ + mock_call( + { + "certificate_response": { + "certificate_action": action, + "exi_response": exi_response, + "status": response_status, + } + } + ) + ] + assert uncalled_mock.mock_calls == [] + + for mock in mock_cmd_set_get_certificate_response.values(): + mock.reset_mock() + + # ************************************************************************************************ + # Use Case M3: Install CA certificate in a Charging Station + # ************************************************************************************************ + + @pytest.mark.parametrize( + "skip_implementation", [{"ProbeModuleSecurity": ["get_installed_certificates"]}] + ) + @pytest.mark.asyncio + async def test_m3_get_installed_certificates( + self, central_system: CentralSystem, probe_module: ProbeModule + ): + """ + Integration test for use case M03 - Retrieve list of available certificates from a Charging Station + The EvseSecurity module is mocked up by the ProbeModule here. + + Tests requirements M03.FR.01, M03.FR.03, M03.FR.04, M03.FR.05 (by checking response from security module is forwarded) + """ + + # Data that is returned by the mocked EvSecurity Module: dict[str, CertificateHashDataChain] + # see type definitions evse_security + mock_certificate_hash_data_chain_data = { + "CSMSRootCertificate": { + "certificate_type": "CSMSRootCertificate", + "certificate_hash_data": { + "hash_algorithm": "SHA256", + "issuer_key_hash": "mock key_hash", + "issuer_name_hash": "mock issuer_name_key_hash", + "serial_number": "1", + }, + }, + "V2GCertificateChain": { + "certificate_type": "V2GCertificateChain", + "certificate_hash_data": { + "hash_algorithm": "SHA256", + "issuer_key_hash": "mock key_hash", + "issuer_name_hash": "mock issuer_name_key_hash", + "serial_number": "2", + }, + "child_certificate_hash_data": [ + { + "hash_algorithm": "SHA256", + "issuer_key_hash": "mock key_hash", + "issuer_name_hash": "mock issuer_name_key_hash", + "serial_number": "3", + }, + { + "hash_algorithm": "SHA256", + "issuer_key_hash": "mock key_hash", + "issuer_name_hash": "mock issuer_name_key_hash", + "serial_number": "4", + }, + ], + }, + } + + # Setup: Probe module mimics security module's get_installed_certificates command + def security_module_get_certs_mock(args): + return { + "status": "Accepted", + "certificate_hash_data_chain": [ + copy.deepcopy( + mock_certificate_hash_data_chain_data[certificate_type] + ) + for certificate_type in args["certificate_types"] + ], + } + + security_module_mock = Mock() + security_module_mock.side_effect = security_module_get_certs_mock + probe_module.implement_command( + "ProbeModuleSecurity", "get_installed_certificates", security_module_mock + ) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Act: request certs + ocpplib_result: call_result201.GetInstalledCertificateIdsPayload = ( + await chargepoint_with_pm.get_installed_certificate_ids_req( + certificate_type=[ + GetCertificateIdUseType.csms_root_certificate, + GetCertificateIdUseType.v2g_certificate_chain, + ] + ) + ) + + # Verfiy + assert ocpplib_result == call_result201.GetInstalledCertificateIdsPayload( + status="Accepted", + certificate_hash_data_chain=[ + mock_certificate_hash_data_chain_data["CSMSRootCertificate"], + mock_certificate_hash_data_chain_data["V2GCertificateChain"], + ], + ) + assert security_module_mock.mock_calls == [ + mock_call( + {"certificate_types": ["CSMSRootCertificate", "V2GCertificateChain"]} + ) + ] + + @pytest.mark.parametrize( + "skip_implementation", [{"ProbeModuleSecurity": ["get_installed_certificates"]}] + ) + async def test_m3_get_installed_certificates_not_found( + self, central_system: CentralSystem, probe_module: ProbeModule + ): + """ + Integration test for use case M03 - Retrieve list of available certificates from a Charging Station, but certificate is not found + The EvseSecurity module is mocked up by the ProbeModule here. + + Tests requirements M03.FR.01, M03.FR.02 + """ + + # Data that is returned by the mocked EvSecurity Module: dict[str, CertificateHashDataChain] + # see type definitions evse_security + + # Setup: Probe module mimics security module's get_installed_certificates command + def security_module_get_certs_mock(args): + return {"status": "NotFound", "certificate_hash_data_chain": []} + + security_module_mock = Mock() + security_module_mock.side_effect = security_module_get_certs_mock + probe_module.implement_command( + "ProbeModuleSecurity", "get_installed_certificates", security_module_mock + ) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Act: request certs + ocpplib_result: call_result201.GetInstalledCertificateIdsPayload = ( + await chargepoint_with_pm.get_installed_certificate_ids_req( + certificate_type=[ + GetCertificateIdUseType.csms_root_certificate, + GetCertificateIdUseType.v2g_certificate_chain, + ] + ) + ) + + # Verfiy + assert ocpplib_result == call_result201.GetInstalledCertificateIdsPayload( + status="NotFound", certificate_hash_data_chain=None + ) + assert security_module_mock.mock_calls == [ + mock_call( + {"certificate_types": ["CSMSRootCertificate", "V2GCertificateChain"]} + ) + ] + + # ************************************************************************************************ + # Use Case M04 - Delete a specific certificate from a Charging Station + # ************************************************************************************************ + + @pytest.mark.parametrize( + "skip_implementation", [{"ProbeModuleSecurity": ["delete_certificate"]}] + ) + @pytest.mark.parametrize("response_status", ["Accepted", "Failed", "NotFound"]) + async def test_m4_delete( + self, + response_status, + central_system: CentralSystem, + probe_module: ProbeModule, + test_utility: TestUtility, + ): + """ + Integration test for use case M04 - Delete a specific certificate from a Charging Station + The EvseSecurity module is mocked up by the ProbeModule here. + + Tests requirements M04.FR.01, M03.FR.02, M03.FR.03, M03.FR.04; only implicitly M03.FR.06, M03.FR.07 M03.FR.08 (this is forwarded to the + mocked security module) + """ + + cert_hash_data: dict[str, str] = { + "hash_algorithm": "SHA256", + "issuer_key_hash": "89ea6977e786fcbaeb4f04e4ccdbfaa6a6088e8ba8f7404033ac1b3a62bc36a1", + "issuer_name_hash": "e60bd843bf2279339127ca19ab6967081dd6f95e745dc8b8632fa56031debe5b", + "serial_number": "1", + } + + # setup probe module additional functions + + security_module_mock = Mock() + security_module_mock.side_effect = [response_status] + + probe_module.implement_command( + "ProbeModuleSecurity", "delete_certificate", security_module_mock + ) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + await chargepoint_with_pm.delete_certificate_req( + certificate_hash_data=copy.deepcopy(cert_hash_data) + ) + assert await wait_for_and_validate( + test_utility, + chargepoint_with_pm, + "DeleteCertificate", + call_result201.DeleteCertificatePayload(response_status), + ) + assert security_module_mock.mock_calls == [ + mock_call({"certificate_hash_data": cert_hash_data}) + ] + + # ************************************************************************************************ + # Use Case M5: M05 - Install CA certificate in a Charging Station + # ************************************************************************************************ + + @pytest.mark.parametrize( + "skip_implementation", [{"ProbeModuleSecurity": ["install_ca_certificate"]}] + ) + @pytest.mark.parametrize( + "evse_security_response_status, chargepoint_response_status", + [ + ("Accepted", "Accepted"), + ("WriteError", "Failed"), + ("InvalidFormat", "Rejected"), + ], + ) + async def test_m5_install( + self, + evse_security_response_status, + chargepoint_response_status, + test_config: OcppTestConfiguration, + central_system: CentralSystem, + probe_module: ProbeModule, + test_utility: TestUtility, + ): + """ + Integration test for use case M05 - Install CA certificate in a Charging Station + The EvseSecurity module is mocked up by the ProbeModule here. + + Tested requirements: M05.FR.01, M05.FR.02, M05.FR.03, M05.FR.06, M05.FR.07; remaining only implicit + """ + + request = { + "certificate_type": "CSMSRootCertificate", + "certificate": "mock certificate", + } + + security_module_mock = Mock() + security_module_mock.side_effect = [evse_security_response_status] + probe_module.implement_command( + "ProbeModuleSecurity", "install_ca_certificate", security_module_mock + ) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + await chargepoint_with_pm.install_certificate_req(**request) + assert await wait_for_and_validate( + test_utility, + chargepoint_with_pm, + "InstallCertificate", + call_result201.InstallCertificatePayload(chargepoint_response_status), + ) + + assert security_module_mock.mock_calls == [ + mock_call( + {"certificate": request["certificate"], "certificate_type": "CSMS"} + ) + ] + + +# ************************************************************************************************ +# E2E Tests +# ************************************************************************************************ + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config("everest-config-ocpp201.yaml") +@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs") +@pytest.mark.use_temporary_persistent_store +class TestIso15118CertificateManagementE2E: + """ + E2E Tests between the mocked CSMS and the SIL Everest stack for Iso15118 Certificate Management. + """ + + @pytest.mark.parametrize( + "certificate_type, use_type, certificate_file", + [ + ( + "CSMSRootCertificate", + GetCertificateIdUseType.csms_root_certificate, + "csms/CSMS_ROOT_CA.pem", + ), + ( + "ManufacturerRootCertificate", + GetCertificateIdUseType.manufacturer_root_certificate, + "mf/MF_ROOT_CA.pem", + ), + ( + "V2GRootCertificate", + GetCertificateIdUseType.v2g_root_certificate, + "v2g/V2G_ROOT_CA.pem", + ), + ( + "MORootCertificate", + GetCertificateIdUseType.mo_root_certificate, + "mo/MO_ROOT_CA.pem", + ), + ], + ) + async def test_m3_retrieve_installed_certificates( + self, + certificate_type, + use_type, + certificate_file, + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + ): + search_path = test_config.certificate_info.csms_root_ca.parent.parent + + result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[use_type] + ) + ) + + assert result == call_result201.GetInstalledCertificateIdsPayload( + status=GetInstalledCertificateStatusType.accepted, + certificate_hash_data_chain=[ + { + "certificate_hash_data": CertificateHashDataGenerator.get_hash_data( + certificate_path=search_path / certificate_file + ), + "certificate_type": certificate_type, + } + ], + ) + + async def test_m3_retrieve_installed_certificates_all_types( + self, test_config: OcppTestConfiguration, charge_point_v201: ChargePoint201 + ): + """ + Mimics OCTT M_ISO_15118_CertificateManagement_CS - TC_M_18_CS-Retrieve certificates from Charging Station + """ + search_path = test_config.certificate_info.csms_root_ca.parent.parent + + result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req() + ) + assert result.status == "Accepted" + + expected_certificate_hash_data_chain_set = { + CertificateHashDataChainEntry.from_dict( + { + "certificate_hash_data": CertificateHashDataGenerator.get_hash_data( + certificate_path=search_path / certificate_file + ), + "certificate_type": certificate_type, + } + ) + for certificate_type, use_type, certificate_file in [ + ( + "CSMSRootCertificate", + GetCertificateIdUseType.csms_root_certificate, + "csms/CSMS_ROOT_CA.pem", + ), + ( + "ManufacturerRootCertificate", + GetCertificateIdUseType.manufacturer_root_certificate, + "mf/MF_ROOT_CA.pem", + ), + ( + "MORootCertificate", + GetCertificateIdUseType.mo_root_certificate, + "mo/MO_ROOT_CA.pem", + ), + ( + "V2GRootCertificate", + GetCertificateIdUseType.v2g_root_certificate, + "v2g/V2G_ROOT_CA.pem", + ), + ] + } | { + CertificateHashDataChainEntry.from_dict(d) + for d in self._get_v2g_certificate_chain(search_path) + } + + assert { + CertificateHashDataChainEntry.from_dict(d) + for d in result.certificate_hash_data_chain + } == expected_certificate_hash_data_chain_set + + async def test_m3_retrieve_installed_certificates_not_found( + self, + tmp_path, + # todo: replace by evse security config fixture + charge_point_v201: ChargePoint201, + ): + """ + Mimics OCTT M_ISO_15118_CertificateManagement_CS - TC_M_19_CS-Retrieve certificates from Charging Station + """ + + # Prerequisite of TC_M_19_CS-Retrieve : "The Charging Station does not have a MORootCertificate installed." + + for f in list((tmp_path / "certs/ca/mo").glob("*.pem")) + list( + (tmp_path / "certs/ca/mo").glob("*.der") + ): + f.unlink() + + result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[GetCertificateIdUseType.mo_root_certificate] + ) + ) + assert result == call_result201.GetInstalledCertificateIdsPayload( + status="NotFound" + ) + + def _get_v2g_certificate_chain(self, ca_cert_path: Path): + root_ca_path = ca_cert_path / "v2g/V2G_ROOT_CA.pem" + sub_ca_1_path = ca_cert_path / "cso/CPO_SUB_CA1.pem" + sub_ca_2_path = ca_cert_path / "cso/CPO_SUB_CA2.pem" + leaf_cert_path = ca_cert_path.parent / "client/cso/SECC_LEAF.pem" + + exp_hashdata_ca_1 = CertificateHashDataGenerator.get_hash_data( + certificate_path=sub_ca_1_path, issuer_certificate_path=root_ca_path + ) + exp_hashdata_ca_2 = CertificateHashDataGenerator.get_hash_data( + certificate_path=sub_ca_2_path, issuer_certificate_path=sub_ca_1_path + ) + exp_hashdata_leaf = CertificateHashDataGenerator.get_hash_data( + certificate_path=leaf_cert_path, issuer_certificate_path=sub_ca_2_path + ) + + return [ + { + "certificate_type": "V2GCertificateChain", + "certificate_hash_data": exp_hashdata_leaf, + "child_certificate_hash_data": [exp_hashdata_ca_1, exp_hashdata_ca_2], + } + ] + + async def test_m3_retrieve_v2g_certificate_chain( + self, test_config: OcppTestConfiguration, charge_point_v201: ChargePoint201 + ): + """ + Quoting the OCPP 2.0.1. spec, req. M03.FR.05: + The Charging Station SHALL include the hash data for each + installed certificate belonging to a V2G certificate chain. Sub CA + certificates SHALL be placed as a childCertificate under the V2G + Charging Station certificate. + + This means that we expect one entry for each v2g leaf cert, with the sub-CAs added as child certificates + The v2g root should not be included in the chain. + The leaf cert is available at certs/client/cso/SECC_LEAF.pem + the sub-CA certs are available at certs/ca/cso/CPO_SUB_CA{1,2}.pem + """ + + # Prepare: Expected hash data + use_type = GetCertificateIdUseType.v2g_certificate_chain + + # Act + result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[use_type] + ) + ) + + # Verify + assert result.status == GetInstalledCertificateStatusType.accepted + + resulting_chain = CertificateHashDataChain.from_list( + result.certificate_hash_data_chain + ) + expected_chain = CertificateHashDataChain.from_list( + self._get_v2g_certificate_chain( + test_config.certificate_info.csms_root_ca.parent.parent + ) + ) + assert resulting_chain == expected_chain + + @pytest.mark.parametrize( + "certificate_type, use_type, certificate_file", + [ + ( + "CSMSRootCertificate", + GetCertificateIdUseType.csms_root_certificate, + "csms/CSMS_ROOT_CA.pem", + ), + ( + "ManufacturerRootCertificate", + GetCertificateIdUseType.manufacturer_root_certificate, + "mf/MF_ROOT_CA.pem", + ), + ( + "V2GRootCertificate", + GetCertificateIdUseType.v2g_root_certificate, + "v2g/V2G_ROOT_CA.pem", + ), + ( + "MORootCertificate", + GetCertificateIdUseType.mo_root_certificate, + "mo/MO_ROOT_CA.pem", + ), + ], + ) + async def test_m4_delete_and_retrieve_certificates( + self, + certificate_type, + use_type, + certificate_file, + test_config, + charge_point_v201: ChargePoint201, + ): + certificate_search_path = ( + test_config.certificate_info.csms_root_ca.parent.parent + ) + + certificate_for_deletion_hash_data = CertificateHashDataGenerator.get_hash_data( + certificate_path=certificate_search_path / certificate_file + ) + + deletion_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.delete_certificate_req( + certificate_hash_data=certificate_for_deletion_hash_data + ) + ) + + assert deletion_result.status == GetInstalledCertificateStatusType.accepted + + verification_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[use_type] + ) + ) + + assert verification_result.status == GetInstalledCertificateStatusType.notFound + + async def test_m4_reject_deletion_of_charging_station_certificate( + self, test_config, charge_point_v201 + ): + """ + M_ISO_15118_CertificateManagement_CS - TC_M_23_CS-Delete a certificate from a Charging Station + """ + + cso_certificate = ( + test_config.certificate_info.csms_root_ca.parent.parent.parent + / "client" + / "csms" + / "CSMS_RSA.pem" + ) + issuer_certificate = ( + test_config.certificate_info.csms_root_ca.parent.parent.parent + / "ca" + / "csms" + / "CSMS_ROOT_CA.pem" + ) + certificate_for_deletion_hash_data = CertificateHashDataGenerator.get_hash_data( + certificate_path=cso_certificate, issuer_certificate_path=issuer_certificate + ) + + deletion_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.delete_certificate_req( + certificate_hash_data=certificate_for_deletion_hash_data + ) + ) + + assert deletion_result.status == "Failed" + + async def test_m4_allow_deletion_of_secc_leaf_certificate( + self, test_config, charge_point_v201 + ): + """ + M_ISO_15118_CertificateManagement_CS - TC_M_23_CS-Delete a certificate from a Charging Station + """ + + cso_certificate = ( + test_config.certificate_info.csms_root_ca.parent.parent.parent + / "client" + / "cso" + / "SECC_LEAF.pem" + ) + issuer_certificate = ( + test_config.certificate_info.csms_root_ca.parent.parent.parent + / "ca" + / "cso" + / "CPO_SUB_CA2.pem" + ) + certificate_for_deletion_hash_data = CertificateHashDataGenerator.get_hash_data( + certificate_path=cso_certificate, issuer_certificate_path=issuer_certificate + ) + + deletion_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.delete_certificate_req( + certificate_hash_data=certificate_for_deletion_hash_data + ) + ) + + assert deletion_result.status == "Accepted" + + @pytest.mark.parametrize( + "certificate_type, ocpp_certificate_type", + [ + ("CSMSRootCertificate", GetCertificateIdUseType.csms_root_certificate), + ( + "ManufacturerRootCertificate", + GetCertificateIdUseType.manufacturer_root_certificate, + ), + ("V2GRootCertificate", GetCertificateIdUseType.v2g_root_certificate), + ("MORootCertificate", GetCertificateIdUseType.mo_root_certificate), + ], + ) + async def test_m5_install_ca_certificate( + self, + example_certificate, + certificate_type, + ocpp_certificate_type, + charge_point_v201: ChargePoint201, + ): + certificate, cert_hash_data = ( + example_certificate["certificate"], + example_certificate["certificate_hash_data"], + ) + + certificates_before: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[ocpp_certificate_type] + ) + ) + + logging.info( + f"Installing certificate (serial: {cert_hash_data['serial_number']}) as {ocpp_certificate_type}" + ) + res: call_result201.InstallCertificatePayload = ( + await charge_point_v201.install_certificate_req( + certificate_type=ocpp_certificate_type, certificate=certificate + ) + ) + + assert res == call_result201.InstallCertificatePayload(status="Accepted") + + verification_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[ocpp_certificate_type] + ) + ) + assert verification_result.status == "Accepted" + assert { + CertificateHashDataChainEntry.from_dict(d) + for d in verification_result.certificate_hash_data_chain + } == { + CertificateHashDataChainEntry.from_dict( + { + "certificate_hash_data": cert_hash_data, + "certificate_type": certificate_type, + } + ) + } | { + CertificateHashDataChainEntry.from_dict(d) + for d in certificates_before.certificate_hash_data_chain + } + + async def test_m5_reject_installation_of_expired_certificate( + self, charge_point_v201 + ): + """Mimics M_ISO_15118_CertificateManagement_CS - TC_M_07_CS-Install CA certificate""" + + cert = """-----BEGIN CERTIFICATE----- +MIICvzCCAacCAhI0MA0GCSqGSIb3DQEBBAUAMCUxCzAJBgNVBAYTAkRFMRYwFAYD +VQQDDA1FeHBpcmVkUm9vdENBMB4XDTAxMTIxMzAwMDAwMFoXDTAyMTIxMzAwMDAw +MFowJTELMAkGA1UEBhMCREUxFjAUBgNVBAMMDUV4cGlyZWRSb290Q0EwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6/YDFyLjc/NrhEGAojh3xxpIhYLWf +2xQ+8Feh/YLS3S7Jeavz4uJykbKqndbdjYK/XMmMKsh/oL6UxZERM9qxeSzIsP+b +p5U6boepS5RE71NhyLjHqLKb2fm3Zrn3UX0dHVt1R3OUSqSzU33hU84kqNd90zBJ +hTmyFokHjklpElt6NkiU94aA/A5hlXxNw5f+bGdHWz2blmzH0ZElCcX4yQ5Q9YZ4 +liXz6rVw5WiLBf3xCOpATxvbWjYuqmd6JhmfVj8vwkLsKSfGmEGQyth1C91enf4I +s06hXg2hHpJq3AfYVTEpYNa/6l6H3lhGbM/jdbBLVSBSs9ceSi/jFPe3AgMBAAEw +DQYJKoZIhvcNAQEEBQADggEBAJDbF55CQYaULoOnLmXGi/RRBL9V5MyeVAsIk2/k +oKVJILGTejGD7WSFvnQF9OS2DhI0TPoxfbpRbZ7vrQ6yA9ddW0xXofnglSPwoAiR +5HMMSk/N5FTHtfArhwz6lltYexeBtYeRbCEilphGHaYPl0dIdBaFay8nm5SwHtFD +gUN7wPcaUwSfD+DnLJGYxcui8eUlpM6o+xUxQ41EdKgCpE/4hkTZz3osmtYph/yG +EZH3hVCk+BjehE9B/9CvbnLiukasDewAxjctSOPJrP2Z58+RRiXiQodeoDVxxvG4 +Pjw/OEvVm/QqKQQDc2q2ZIs8RsvbpeNZD84mJT706EqID3s= +-----END CERTIFICATE-----""" + + expired_certificate = cert + + res: call_result201.InstallCertificatePayload = ( + await charge_point_v201.install_certificate_req( + certificate_type=GetCertificateIdUseType.csms_root_certificate, + certificate=expired_certificate, + ) + ) + + assert res == call_result201.InstallCertificatePayload(status="Rejected") + + @pytest.mark.parametrize( + "certificate_type, ocpp_certificate_type", + [ + ("CSMSRootCertificate", GetCertificateIdUseType.csms_root_certificate), + ( + "ManufacturerRootCertificate", + GetCertificateIdUseType.manufacturer_root_certificate, + ), + ("V2GRootCertificate", GetCertificateIdUseType.v2g_root_certificate), + ("MORootCertificate", GetCertificateIdUseType.mo_root_certificate), + ], + ) + async def test_m4_delete_installed_certificates( + self, + certificate_type, + ocpp_certificate_type, + example_certificate, + test_config, + charge_point_v201: ChargePoint201, + ): + certificate, cert_hash_data = ( + example_certificate["certificate"], + example_certificate["certificate_hash_data"], + ) + + certificates_before: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[ocpp_certificate_type] + ) + ) + assert certificates_before.status == GetInstalledCertificateStatusType.accepted + + installation_result: call_result201.InstallCertificatePayload = ( + await charge_point_v201.install_certificate_req( + certificate_type=ocpp_certificate_type, certificate=certificate + ) + ) + assert installation_result == call_result201.InstallCertificatePayload( + status="Accepted" + ) + + certificates_after_install: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[ocpp_certificate_type] + ) + ) + + assert cert_hash_data in [ + c["certificate_hash_data"] + for c in certificates_after_install.certificate_hash_data_chain + ] + + deletion_result: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.delete_certificate_req( + certificate_hash_data=cert_hash_data + ) + ) + + assert deletion_result.status == GetInstalledCertificateStatusType.accepted + + certificates_after_delete: call_result201.GetInstalledCertificateIdsPayload = ( + await charge_point_v201.get_installed_certificate_ids_req( + certificate_type=[ocpp_certificate_type] + ) + ) + + assert ( + certificates_after_delete.status + == GetInstalledCertificateStatusType.accepted + ) + assert ( + certificates_before.certificate_hash_data_chain + == certificates_after_delete.certificate_hash_data_chain + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/local_authorization_list.py b/tests/ocpp_tests/test_sets/ocpp201/local_authorization_list.py new file mode 100644 index 000000000..ef7aabf9d --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/local_authorization_list.py @@ -0,0 +1,834 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +# fmt: off +import pytest +import logging + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode +from everest.testing.ocpp_utils.fixtures import * + +from everest_test_utils import * +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum) +from ocpp.v201.enums import * +from ocpp.v201.datatypes import * +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 +from ocpp.routing import on, create_route_map +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +# fmt: on + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_D01_D02( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + + id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443) + id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443) + id_token_125 = IdTokenType(id_token="125", type=IdTokenTypeEnum.iso14443) + + id_token_accepted = IdTokenInfoType(status=AuthorizationStatusType.accepted) + id_token_blocked = IdTokenInfoType(status=AuthorizationStatusType.blocked) + + # D02.FR.01 + async def check_list_version(expected_version: int): + r: call_result201.GetLocalListVersionPayload = ( + await charge_point_v201.get_local_list_version() + ) + assert r.version_number == expected_version + + # D01.FR.12 + async def check_list_size(expected_size: int): + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Entries" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == str(expected_size) + + # LocalAuthListCtrlr needs to be avaialable + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Available" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == "true" + + # Enable local list + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "LocalAuthListCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # D02.FR.02 LocalAuthListEnabled is true amd CSMS has not sent any update + await check_list_version(0) + await check_list_size(0) + + # D01.FR.18 VersionNumber shall be greater than 0 (we fail otherwise) + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=0, update_type=UpdateType.full + ) + ) + assert r.status == SendLocalListStatusType.failed + + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=0, update_type=UpdateType.differential + ) + ) + assert r.status == SendLocalListStatusType.failed + + await check_list_version(0) + await check_list_size(0) + + # D01.FR.01 + # D01.FR.02 + # Add first list version + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=10, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(10) + await check_list_size(2) + + # D01.FR.04 + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=20, update_type=UpdateType.full + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(20) + await check_list_size(0) + + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=12, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_125, id_token_info=id_token_blocked + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(12) + await check_list_size(3) + + # D01.FR.05 + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=15, update_type=UpdateType.differential + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(15) + await check_list_size(3) + + # D01.FR.06 + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=25, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.failed + + await check_list_version(15) + await check_list_size(3) + + # idTokenInfo is required when UpdateType is full + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=3, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData(id_token=id_token_124), + ], + ) + ) + assert r.status == SendLocalListStatusType.failed + + await check_list_version(15) + await check_list_size(3) + + # D01.FR.15 + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=25, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(25) + await check_list_size(2) + + # D01.FR.16 Update + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=26, + update_type=UpdateType.differential, + local_authorization_list=[ + AuthorizationData(id_token=id_token_123, id_token_info=id_token_blocked) + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(26) + await check_list_size(2) + + # D01.FR.16 Add + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=27, + update_type=UpdateType.differential, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_125, id_token_info=id_token_accepted + ) + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(27) + await check_list_size(3) + + # D01.FR.17 Remove if empty idTokenInfo + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=28, + update_type=UpdateType.differential, + local_authorization_list=[AuthorizationData(id_token=id_token_123)], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(28) + await check_list_size(2) + + # D01.FR.19 Smaller or equal version_number should be ignored with status set to VersionMismatch + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=27, + update_type=UpdateType.differential, + local_authorization_list=[AuthorizationData(id_token=id_token_125)], + ) + ) + assert r.status == SendLocalListStatusType.version_mismatch + + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=28, + update_type=UpdateType.differential, + local_authorization_list=[AuthorizationData(id_token=id_token_125)], + ) + ) + assert r.status == SendLocalListStatusType.version_mismatch + + await check_list_version(28) + await check_list_size(2) + + # D01.FR.13 + # Disable auth list again to check if version returns to 0 + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "LocalAuthListCtrlr", "Enabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # D02.FR.03: Always return 0 when LocalAuthListEnabled is false + await check_list_version(0) + + # Disabled so should not be able to send list + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=1, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.failed + + +async def prepare_auth_cache( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, + accepted_tags: [], + rejected_tags: [], +): + # Prepare the cache with valid and invalid tags + + def get_token_info(token: str): + if token in accepted_tags: + return IdTokenInfoType(status=AuthorizationStatusType.accepted) + else: + return IdTokenInfoType(status=AuthorizationStatusType.blocked) + + @on(Action.Authorize) + def on_authorize(**kwargs): + msg = call201.AuthorizePayload(**kwargs) + msg_token = IdTokenType(**msg.id_token) + return call_result201.AuthorizePayload( + id_token_info=get_token_info(msg_token.id_token) + ) + + setattr(charge_point_v201, "on_authorize", on_authorize) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + test_utility.validation_mode = ValidationMode.STRICT + for tag in accepted_tags: + test_controller.swipe(tag) + test_controller.plug_in() + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started"}, + ) + test_controller.swipe(tag) + test_controller.plug_out() + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + for tag in rejected_tags: + test_controller.swipe(tag) + assert await wait_for_and_validate( + test_utility, charge_point_v201, "Authorize", {"idToken": {"idToken": tag}} + ) + + test_utility.validation_mode = ValidationMode.EASY + test_utility.messages.clear() + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_C13( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + + # LocalAuthListCtrlr needs to be avaialable + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Available" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == "true" + + # Enable local list + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "LocalAuthListCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set OfflineThreshold + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "OCPPCommCtrlr", "OfflineThreshold", "2" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Disable offline tx for unknown id + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443) + id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443) + id_token_125 = IdTokenType(id_token="125", type=IdTokenTypeEnum.iso14443) + + id_token_accepted = IdTokenInfoType(status=AuthorizationStatusType.accepted) + id_token_blocked = IdTokenInfoType(status=AuthorizationStatusType.blocked) + + async def check_list_version(expected_version: int): + r: call_result201.GetLocalListVersionPayload = ( + await charge_point_v201.get_local_list_version() + ) + assert r.version_number == expected_version + + async def check_list_size(expected_size: int): + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Entries" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == str(expected_size) + + await prepare_auth_cache( + charge_point_v201=charge_point_v201, + central_system_v201=central_system_v201, + test_controller=test_controller, + test_utility=test_utility, + accepted_tags=[id_token_123.id_token, id_token_125.id_token], + rejected_tags=[id_token_124.id_token], + ) + + # Add first list version + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=10, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_125, id_token_info=id_token_blocked + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(10) + await check_list_size(3) + + test_utility.forbidden_actions.append("Authorize") + + # C13.FR.02 + # C13.FR.03 + # Valid token in the local list may be authorized offline + # Check AuthList: Valid, Cache: Invalid + # Expected result: Start session + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + test_controller.swipe(id_token_123.id_token) + test_controller.plug_in() + + await asyncio.sleep(2) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Started", + "idToken": {"idToken": id_token_123.id_token, "type": "ISO14443"}, + }, + ) + + test_controller.swipe(id_token_123.id_token) + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + # C13.FR.01 + # Invalid token in local list may not be authorized + # Check AuthList: Invalid, Cache: Valid + # Expected result: No session started + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + test_controller.swipe(id_token_125.id_token) + test_controller.plug_in() + + await asyncio.sleep(5) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + test_utility.messages.clear() + test_utility.forbidden_actions.append("TransactionEvent") + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + test_controller.plug_out() + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + test_utility.forbidden_actions.remove("TransactionEvent") + + # C13.FR.04 + # With OfflineTxForUnknownIdEnabled == true + # Invalid token in local list may not be authorized + # Unkown token may be authorized + # See errata for case C13.FR.04 + + # Enable offline tx for unknown id + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + logging.info("disconnect the ws connection...") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + test_controller.swipe(id_token_125.id_token) + test_controller.swipe("unknown") + test_controller.plug_in() + + await asyncio.sleep(5) + + logging.info("connecting the ws connection") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "idToken": {"idToken": "unknown", "type": "ISO14443"}}, + ) + + test_controller.plug_out() + test_controller.swipe("unknown") + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_C14( + charge_point_v201: ChargePoint201, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + + # LocalAuthListCtrlr needs to be avaialable + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Available" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == "true" + + # AuthCacheCtrlr needs to be avaialable + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req("AuthCacheCtrlr", "Available") + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == "true" + + # Enable local list + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "LocalAuthListCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Disable offline tx for unknown id + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443) + id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443) + + id_token_accepted = IdTokenInfoType(status=AuthorizationStatusType.accepted) + id_token_blocked = IdTokenInfoType(status=AuthorizationStatusType.blocked) + + async def check_list_version(expected_version: int): + r: call_result201.GetLocalListVersionPayload = ( + await charge_point_v201.get_local_list_version() + ) + assert r.version_number == expected_version + + async def check_list_size(expected_size: int): + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "LocalAuthListCtrlr", "Entries" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + assert get_variables_result.attribute_value == str(expected_size) + + await prepare_auth_cache( + charge_point_v201=charge_point_v201, + central_system_v201=central_system_v201, + test_controller=test_controller, + test_utility=test_utility, + accepted_tags=[id_token_123.id_token], + rejected_tags=[id_token_124.id_token], + ) + + # Add first list version + r: call_result201.SendLocalListPayload = ( + await charge_point_v201.send_local_list_req( + version_number=10, + update_type=UpdateType.full, + local_authorization_list=[ + AuthorizationData( + id_token=id_token_124, id_token_info=id_token_accepted + ), + AuthorizationData( + id_token=id_token_123, id_token_info=id_token_blocked + ), + ], + ) + ) + assert r.status == SendLocalListStatusType.accepted + + await check_list_version(10) + await check_list_size(2) + + await asyncio.sleep(1) + + # C14.FR.02 + # Check AuthList: Valid, Cache: Invalid + # Expected result: Start session without authorizeReq + test_utility.messages.clear() + test_utility.forbidden_actions.append("Authorize") + + test_controller.swipe(id_token_124.id_token) + test_controller.plug_in() + + test_utility.validation_mode = ValidationMode.STRICT + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Started", + "idToken": {"idToken": id_token_124.id_token, "type": "ISO14443"}, + }, + ) + + test_utility.validation_mode = ValidationMode.EASY + + await asyncio.sleep(1) + + test_controller.swipe(id_token_124.id_token) + test_controller.plug_out() + + test_utility.validation_mode = ValidationMode.STRICT + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + test_utility.validation_mode = ValidationMode.EASY + + test_utility.forbidden_actions.remove("Authorize") + + # C14.FR.01 + # C14.FR.03 + # Check AuthList: Invalid, Cache: Valid + # Expected result: Send autorize request + + await asyncio.sleep(1) + test_utility.messages.clear() + + test_controller.swipe(id_token_123.id_token) + test_controller.plug_in() + + test_utility.validation_mode = ValidationMode.STRICT + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload(id_token=id_token_123), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Started", + "idToken": {"idToken": id_token_123.id_token, "type": "ISO14443"}, + }, + ) + + test_utility.validation_mode = ValidationMode.EASY + + test_controller.swipe(id_token_123.id_token) + test_controller.plug_out() + + test_utility.validation_mode = ValidationMode.STRICT + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"connectorStatus": "Available", "evseId": 1}, + ) + test_utility.validation_mode = ValidationMode.EASY diff --git a/tests/ocpp_tests/test_sets/ocpp201/meterValues.py b/tests/ocpp_tests/test_sets/ocpp201/meterValues.py new file mode 100644 index 000000000..403472991 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/meterValues.py @@ -0,0 +1,180 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +# fmt: off +import pytest +from datetime import datetime +import logging + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +from validations import * +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * + +from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum, SetVariableStatusType, ConnectorStatusType,GetVariableStatusType) +from ocpp.v201.datatypes import * +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 + +# fmt: on + +log = logging.getLogger("meterValues") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_J01_19( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + J01.FR.19 + ... + """ + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + evse_id2 = 2 + + # make an unknown IdToken + id_tokenJ01 = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443) + + log.info( + "##################### J01.FR.19: Sending Meter Values not related to a transaction #################" + ) + test_utility.messages.clear() + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + # Configure AlignedDataInterval + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AlignedDataCtrlr", "Interval", "3" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Configure SampledDataInterval + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SampledDataCtrlr", "TxUpdatedInterval", "3" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Configure AlignedDataInterval + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AlignedDataCtrlr", "SendDuringIdle", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Configure PhaseRotation + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "ChargingStation", "PhaseRotation", "TRS" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Get the value of PhaseRotation + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "ChargingStation", "PhaseRotation" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + if get_variables_result.attribute_status == GetVariableStatusType.accepted: + log.info("Phase Rotation %s " % get_variables_result.attribute_value) + + # send meter values periodically when not charging + logging.debug("Collecting meter values...") + for _ in range(3): + # send MeterValues + assert await wait_for_and_validate( + test_utility, charge_point_v201, "MeterValues", {"evseId": 1} + ) + assert await wait_for_and_validate( + test_utility, charge_point_v201, "MeterValues", {"evseId": 2} + ) + + # swipe id tag to authorize + test_controller.swipe(id_tokenJ01.id_token) + + # start charging session + test_controller.plug_in() + + test_utility.messages.clear() + + # when in a middle of a transaction do not send meter values + test_utility.forbidden_actions.append("MeterValues") + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + + for _ in range(3): + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"}, + ) + + # swipe id tag to de-authorize + test_controller.swipe(id_tokenJ01.id_token) + + # stop charging session + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/provisioning.py b/tests/ocpp_tests/test_sets/ocpp201/provisioning.py new file mode 100644 index 000000000..4baeda908 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/provisioning.py @@ -0,0 +1,1415 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +import asyncio +from datetime import datetime + +import traceback +# fmt: off +import logging + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 +from ocpp.v201.enums import * +from ocpp.v201.datatypes import * +from ocpp.routing import on, create_route_map +from everest.testing.ocpp_utils.fixtures import * +from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from ocpp.v201.enums import (Action, SetVariableStatusType, ConnectorStatusType,GetVariableStatusType) +from validations import validate_status_notification_201, validate_notify_report_data_201, wait_for_callerror_and_validate +from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP201ConfigAdjustment +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration, ValidationMode +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +# fmt: on + +log = logging.getLogger("provisioningTest") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_B08_FR_07( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B08.FR.07 + ComponentCriteria contains: Active The Charging Station SHALL report every component that has + the variable Active set to true, or does not have the Active variable in a NotifyReportRequest + """ + + log.info( + " ############################# Test case B08: Get custom report ###############################" + ) + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # set a component variable to true + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "ChargingStatusIndicator", "Active", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r = await charge_point_v201.get_report_req( + request_id=567, component_criteria=[ComponentCriterionType.active] + ) + + exp_single_report_data_active = ReportDataType( + component=ComponentType(name="ChargingStatusIndicator"), + variable=VariableType(name="Active"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, value="true" + ), + ) + + # get the value of component criteria + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=567, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data_active], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_B08_FR_08( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B08.FR.08 + ComponentCriteria contains: Available The Charging Station SHALL report every component that has + the variable Available set to true, or does not have the Available variable in a NotifyReportRequest + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + await charge_point_v201.get_report_req( + request_id=777, component_criteria=[ComponentCriterionType.available] + ) + + exp_single_report_data_avail = ReportDataType( + component=ComponentType(name="AuthCacheCtrlr"), + variable=VariableType(name="Available"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, + value="true", + # mutability=MutabilityType.read_write + ), + ) + + # get the value of component criteria + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=777, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data_avail], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_B08_FR_09( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """B08.FR.09 ComponentCriteria contains: EnabledThe Charging Station SHALL report every component that + has the variable Enabled set to true, or does not have the Enabled variable, in a NotifyReportRequest. + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # Enable some variables with enable + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SampledDataCtrlr", "Enabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r = await charge_point_v201.get_report_req( + request_id=1, + component_criteria=[ComponentCriterionType.enabled], + component_variable=[ + ComponentVariableType(component=ComponentType(name="TxCtrlr")), + ComponentVariableType(component=ComponentType(name="DeviceDataCtrlr")), + ComponentVariableType(component=ComponentType(name="AuthCacheCtrlr")), + ], + ) + + exp_single_report_data = ReportDataType( + component=ComponentType(name="AuthCacheCtrlr"), + variable=VariableType(name="Enabled"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, + value="true", + # mutability=MutabilityType.read_write + ), + variable_characteristics=VariableCharacteristicsType( + data_type=DataType.boolean, supports_monitoring=True + ), + ) + + # get the value of component criteria + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=1, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data], + ), + validate_notify_report_data_201, + ) + + # await asyncio.sleep(3) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_B08_FR_10( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B08.FR.10 + ComponentCriteria contains: ProblemThe Charging Station SHALL report every component that has + the variable Problem set to true in a NotifyReportRequest. + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # set a component variable to true + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "ChargingStation", "Problem", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r = await charge_point_v201.get_report_req( + request_id=45, component_criteria=[ComponentCriterionType.problem] + ) + + exp_single_report_data2 = ReportDataType( + component=ComponentType(name="ChargingStation"), + variable=VariableType(name="Problem"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, + value="true", + # mutability=MutabilityType.read_write + ), + ) + + # get the value of component criteria + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=45, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data2], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "BytesPerMessageGetReport", "Actual" + ), + "42", + ) + ] + ) +) +async def test_B08_FR_18( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B08.FR.18 + Charging Station receives a GetReportRequest with a length of more bytes than allowed by BytesPerMessageGetReport + The Charging Station MAY respond with a CALLERROR(FormatViolation) + + Setup: Set BytesPerMessage to 42 for this test + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # get the value of BytesPerMessage + r: call_result201.GetVariablesPayload = await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="DeviceDataCtrlr"), + variable=VariableType( + name="BytesPerMessage", + instance="GetReport", + ), + attribute_type=AttributeType.actual, + ) + ] + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + bytes_per_message = json.loads(get_variables_result.attribute_value) + log.debug(" max bytes per get report request %d" % bytes_per_message) + + r = await charge_point_v201.get_report_req( + request_id=777, component_criteria=[ComponentCriterionType.available] + ) + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v201, "FormatViolation" + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "ItemsPerMessageGetReport" + ), + "2", + ) + ] + ) +) +async def test_B08_FR_17( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B08.FR.17 + Charging Station receives a GetReportRequest with more ComponentVariableType elements than allowed by ItemsPerMessageGetReport + The Charging Station MAY respond with a CALLERROR(OccurenceConstraintViolation) + + Setup set ItemsPerMessageGetReport to 2 + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # get the value of ItemsPerMessage + r: call_result201.GetVariablesPayload = await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="DeviceDataCtrlr"), + variable=VariableType( + name="ItemsPerMessage", + instance="GetReport", + ), + attribute_type=AttributeType.actual, + ) + ] + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + items_per_message = json.loads(get_variables_result.attribute_value) + log.debug(" max items per get report request %d" % items_per_message) + + r = await charge_point_v201.get_report_req( + request_id=777, + component_variable=[ + ComponentVariableType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + ), + ComponentVariableType( + component=ComponentType(name="DeviceDataCtrlr"), + variable=VariableType( + name="ItemsPerMessage", + instance="GetVariables", + ), + ), + ComponentVariableType( + component=ComponentType(name="AlignedDataCtrlr"), + variable=VariableType(name="Measurands"), + ), + ], + ) + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v201, "OccurenceConstraintViolation" + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_TC_B_18_CS( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + TC_B_18_CS + Get Custom Report - with component criteria and component/variable + """ + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + r = await charge_point_v201.get_report_req( + request_id=2534, + component_criteria=[ComponentCriterionType.available], + component_variable=[ + ComponentVariableType( + component=ComponentType(name="EVSE", evse=EVSEType(id=1)), + variable=VariableType(name="AvailabilityState"), + ) + ], + ) + + exp_single_report_data = ReportDataType( + component=ComponentType(name="EVSE", evse=EVSEType(id=1)), + variable=VariableType(name="AvailabilityState"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, value="Available" + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=2534, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data], + ), + validate_notify_report_data_201, + ) + + r: call_result201.GetReportPayload = await charge_point_v201.get_report_req( + request_id=2535, + component_criteria=[ComponentCriterionType.problem], + component_variable=[ + ComponentVariableType( + component=ComponentType(name="EVSE", evse=EVSEType(id=1)), + variable=VariableType(name="AvailabilityState"), + ) + ], + ) + + # should return an empty set + assert r.status == "EmptyResultSet" + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "ItemsPerMessageGetReport" + ), + "4", + ), + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "ItemsPerMessageGetVariables" + ), + "2", + ), + ] + ) +) +async def test_TC_B_54_CS( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + TC_B_54_CS + Get Custom Report - with component/variable, but no instance + """ + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + await charge_point_v201.get_report_req( + request_id=2538, + component_variable=[ + ComponentVariableType( + component=ComponentType( + name="DeviceDataCtrlr", + ), + variable=VariableType(name="ItemsPerMessage"), + ) + ], + ) + + b_54_1 = ReportDataType( + component=ComponentType( + name="DeviceDataCtrlr", + ), + variable=VariableType(name="ItemsPerMessage", instance="GetReport"), + variable_attribute=VariableAttributeType(type=AttributeType.actual, value="4"), + ) + + b_54_2 = ReportDataType( + component=ComponentType( + name="DeviceDataCtrlr", + ), + variable=VariableType(name="ItemsPerMessage", instance="GetVariables"), + variable_attribute=VariableAttributeType(type=AttributeType.actual, value="2"), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=2538, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[b_54_1, b_54_2], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "ItemsPerMessageGetReport" + ), + "4", + ) + ] + ) +) +async def test_TC_B_55_CS( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + TC_B_55_CS + Get Custom Report - with component/variable/instance + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + await charge_point_v201.get_report_req( + request_id=2539, + component_variable=[ + ComponentVariableType( + component=ComponentType( + name="DeviceDataCtrlr", + ), + variable=VariableType(name="ItemsPerMessage", instance="GetReport"), + ) + ], + ) + + b_55_1 = ReportDataType( + component=ComponentType( + name="DeviceDataCtrlr", + ), + variable=VariableType(name="ItemsPerMessage", instance="GetReport"), + variable_attribute=VariableAttributeType(type=AttributeType.actual, value="4"), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=2539, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[b_55_1], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_TC_B_56_CS( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + TC_B_56_CS + Get Custom Report - with component/variable, but no evseId + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + await charge_point_v201.get_report_req( + request_id=2544, + component_variable=[ + ComponentVariableType( + component=ComponentType(name="EVSE"), + variable=VariableType(name="AvailabilityState"), + ) + ], + ) + + b_56_1 = ReportDataType( + component=ComponentType(name="EVSE", evse=EVSEType(id=1)), + variable=VariableType(name="AvailabilityState"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, value="Available" + ), + ) + + b_56_2 = ReportDataType( + component=ComponentType(name="EVSE", evse=EVSEType(id=2)), + variable=VariableType(name="AvailabilityState"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, value="Available" + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=2544, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[b_56_1, b_56_2], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_cold_boot_01( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B01.FR.01 + ... + """ + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint() + + try: + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), ConnectorStatusType.available, 1, 1 + ), + validate_status_notification_201, + ) + except Exception as e: + traceback.print_exc() + logging.critical(e) + + # TOOD(piet): Check configured HeartbeatInterval of BootNotificationResponse + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_cold_boot_pending_01( + test_config: OcppTestConfiguration, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.now().isoformat(), + interval=5, + status=RegistrationStatusType.pending, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatusType.accepted, + ) + + test_utility.forbidden_actions.append("SecurityEventNotification") + + central_system_v201.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint() + + setattr(charge_point_v201, "on_boot_notification", on_boot_notification_accepted) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "BootNotification", {} + ) + + test_utility.forbidden_actions.clear() + + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "SecurityEventNotification", {} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_cold_boot_rejected_01( + test_config: OcppTestConfiguration, + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.now().isoformat(), + interval=5, + status=RegistrationStatusType.rejected, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=5, + status=RegistrationStatusType.accepted, + ) + + central_system_v201.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint() + + setattr(charge_point_v201, "on_boot_notification", on_boot_notification_accepted) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "BootNotification", {} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_set_get_variables_01( + charge_point_v201: ChargePoint201, test_utility: TestUtility +): + + await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_type=AttributeType.actual, + ) + ] + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "GetVariables", + call_result201.GetVariablesPayload( + get_variable_result=[ + GetVariableResultType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_status=GetVariableStatusType.accepted, + attribute_type=AttributeType.actual, + attribute_value="true", + ) + ] + ), + ) + + await charge_point_v201.set_variables_req( + set_variable_data=[ + SetVariableDataType( + attribute_value="false", + attribute_type=AttributeType.actual, + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + ) + ] + ) + + await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_type=AttributeType.actual, + ) + ] + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "GetVariables", + call_result201.GetVariablesPayload( + get_variable_result=[ + GetVariableResultType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_status=GetVariableStatusType.accepted, + attribute_type=AttributeType.actual, + attribute_value="false", + ) + ] + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_set_get_variables_02( + charge_point_v201: ChargePoint201, test_utility: TestUtility +): + await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="InternalCtrlr"), + variable=VariableType(name="ChargePointVendor"), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="OCPPCommCtrlr"), + variable=VariableType(name="UnknownVariable"), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="UnknownComponent"), + variable=VariableType(name="UnknownVariable"), + attribute_type=AttributeType.actual, + ), + ] + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "GetVariables", + call_result201.GetVariablesPayload( + get_variable_result=[ + GetVariableResultType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_status=GetVariableStatusType.accepted, + attribute_type=AttributeType.actual, + attribute_value="true", + ), + GetVariableResultType( + component=ComponentType(name="InternalCtrlr"), + variable=VariableType(name="ChargePointVendor"), + attribute_status=GetVariableStatusType.accepted, + attribute_type=AttributeType.actual, + attribute_value="EVerestVendor", + ), + GetVariableResultType( + component=ComponentType(name="OCPPCommCtrlr"), + variable=VariableType(name="UnknownVariable"), + attribute_status=GetVariableStatusType.unknown_variable, + attribute_type=AttributeType.actual, + ), + GetVariableResultType( + component=ComponentType(name="UnknownComponent"), + variable=VariableType(name="UnknownVariable"), + attribute_status=GetVariableStatusType.unknown_component, + attribute_type=AttributeType.actual, + ), + ] + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_get_base_report_01( + charge_point_v201: ChargePoint201, test_utility: TestUtility +): + await charge_point_v201.get_base_report_req( + request_id=1, report_base=ReportBaseType.full_inventory + ) + + await wait_for_and_validate( + test_utility, + charge_point_v201, + "GetBaseReport", + call_result201.GetBaseReportPayload( + status=GenericDeviceModelStatusType.accepted + ), + ) + + exp_single_report_data = ReportDataType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + variable_attribute=VariableAttributeType( + type=AttributeType.actual, + value="true", + mutability=MutabilityType.read_write, + ), + variable_characteristics=VariableCharacteristicsType( + data_type=DataType.boolean, supports_monitoring=True + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "NotifyReport", + call201.NotifyReportPayload( + request_id=1, + generated_at=datetime.now().isoformat(), + seq_no=0, + report_data=[exp_single_report_data], + ), + validate_notify_report_data_201, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_get_custom_report_01(charge_point_v201: ChargePoint201): + await charge_point_v201.get_report_req( + request_id=1, + component_variable=[ + ComponentVariableType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="NotAValidVariable"), + ), + ], + component_criteria=[ComponentCriterionType.enabled], + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_b09_b10( + charge_point_v201: ChargePoint201, + test_controller: TestController, + central_system_v201: CentralSystem, +): + + # TODO(This discovers a bug in the connectivity_manager of libocpp. this->network_connection_profiles are not updated when a new profile is set) + + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "InternalCtrlr", "NetworkConnectionProfiles" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + + profiles = json.loads(get_variables_result.attribute_value) + assert len(profiles) == 1 + + # invalid security profile + r: call_result201.SetNetworkProfilePayload = ( + await charge_point_v201.set_network_profile_req( + configuration_slot=1, + connection_data=NetworkConnectionProfileType( + ocpp_version=OCPPVersionType.ocpp20, + ocpp_transport=OCPPTransportType.json, + ocpp_csms_url="ws://localhost:9000/cp001", + message_timeout=30, + security_profile=0, + ocpp_interface=OCPPInterfaceType.wired0, + ), + ) + ) + + assert r.status == "Rejected" + + # invalid configuration slot + r: call_result201.SetNetworkProfilePayload = ( + await charge_point_v201.set_network_profile_req( + configuration_slot=100, + connection_data=NetworkConnectionProfileType( + ocpp_version=OCPPVersionType.ocpp20, + ocpp_transport=OCPPTransportType.json, + ocpp_csms_url="ws://localhost:9000/cp001", + message_timeout=30, + security_profile=0, + ocpp_interface=OCPPInterfaceType.wired0, + ), + ) + ) + + assert r.status == "Rejected" + + # valid + r: call_result201.SetNetworkProfilePayload = ( + await charge_point_v201.set_network_profile_req( + configuration_slot=2, + connection_data=NetworkConnectionProfileType( + ocpp_version=OCPPVersionType.ocpp20, + ocpp_transport=OCPPTransportType.json, + ocpp_csms_url="wss://localhost:9000/cp001", + message_timeout=30, + security_profile=2, + ocpp_interface=OCPPInterfaceType.wired0, + ), + ) + ) + + assert r.status == "Accepted" + + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "InternalCtrlr", "NetworkConnectionProfiles" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + + profiles = json.loads(get_variables_result.attribute_value) + assert len(profiles) == 2 + + # Set valid NetworkConfigurationPriority + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "OCPPCommCtrlr", "NetworkConfigurationPriority", "2,1" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "DeviceDataCtrlr", "ItemsPerMessageGetVariables" + ), + "2", + ) + ] + ) +) +async def test_B06_09_16( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B06.FR.09 + B06.FR.16 + """ + log.info( + " ############################# Test case B06: Get variables Request ###############################" + ) + + # When the Charging Station receives a GetVariablesRequest for a Variable in the GetVariableData that is WriteOnly, + # The Charging Station SHALL set the attributeStatus field in the + # corresponding GetVariableResult to: Rejected. + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # Write into Basic Auth Password + r: call_result.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SecurityCtrlr", "BasicAuthPassword", "8BADF00D8BADF00D" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "SecurityCtrlr", "BasicAuthPassword" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.rejected + + # Charging Station receives a GetVariablesRequest with more GetVariableData elements than allowed by ItemsPerMessageGetVariables + # The Charging Station MAY respond with a CALLERROR(OccurenceConstraintViolation) + + # get the value of ItemsPerMessage + r: call_result201.GetVariablesPayload = await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="DeviceDataCtrlr"), + variable=VariableType( + name="ItemsPerMessage", + instance="GetVariables", + ), + attribute_type=AttributeType.actual, + ) + ] + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + items_per_message = json.loads(get_variables_result.attribute_value) + log.debug(" max items per get variables request %d" % items_per_message) + + # request more than max items per message variables + # get the value of ItemsPerMessage + r: call_result201.GetVariablesPayload = await charge_point_v201.get_variables_req( + get_variable_data=[ + GetVariableDataType( + component=ComponentType(name="DeviceDataCtrlr"), + variable=VariableType( + name="ItemsPerMessage", + instance="GetVariables", + ), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="TxCtrlr"), + variable=VariableType(name="StopTxOnInvalidId"), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="AlignedDataCtrlr"), + variable=VariableType(name="Measurands"), + attribute_type=AttributeType.actual, + ), + GetVariableDataType( + component=ComponentType(name="AuthCacheCtrlr"), + variable=VariableType(name="Enabled"), + attribute_type=AttributeType.actual, + ), + ] + ) + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v201, "OccurenceConstraintViolation" + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_B04( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + B04.FR.01 + B04.FR.02 + ... + """ + + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + evse_id2 = 2 + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=3, + status=RegistrationStatusType.accepted, + ) + + central_system_v201.function_overrides.append( + ("on_boot_notification", on_boot_notification_accepted) + ) + + test_utility.validation_mode = ValidationMode.STRICT + + log.info( + " ############################# Test case B04: Offline Idle Behaviour ###############################" + ) + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # setattr(charge_point_v201, 'on_boot_notification_accepted',on_boot_notification_accepted) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + # Set valid OfflineThreshold to 15s + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "OCPPCommCtrlr", "OfflineThreshold", "15" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + test_utility.messages.clear() + + for _ in range(3): + # send HeartBeat request when idle + assert await wait_for_and_validate( + test_utility, charge_point_v201, "Heartbeat", call.HeartbeatPayload() + ) + + test_utility.messages.clear() + + log.debug("========================B04.FR.01=========================") + # Simulate connection loss + test_controller.disconnect_websocket() + + # Wait 20 seconds + await asyncio.sleep(20) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # expect StatusNotification with status available as the disconnect duration was > than offline throeshold + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + test_utility.messages.clear() + + # Wait 5 seconds + await asyncio.sleep(5) + + log.debug("========================B04.FR.02=========================") + + # start charging session + test_controller.plug_in(connector_id=2) + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.occupied, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + # Simulate connection loss + test_controller.disconnect_websocket() + + # Wait 7 seconds + await asyncio.sleep(7) + + # stop charging session + test_controller.plug_out(connector_id=2) + + # Wait 3 seconds + await asyncio.sleep(3) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + test_utility.messages.clear() + + for _ in range(3): + # send HeartBeat request when idle + assert await wait_for_and_validate( + test_utility, charge_point_v201, "Heartbeat", call.HeartbeatPayload() + ) diff --git a/tests/ocpp_tests/test_sets/ocpp201/remote_control.py b/tests/ocpp_tests/test_sets/ocpp201/remote_control.py new file mode 100644 index 000000000..14592ac86 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/remote_control.py @@ -0,0 +1,771 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime +import pytest +# fmt: off +import sys +import os + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +sys.path.append(os.path.abspath( + os.path.join(os.path.dirname(__file__), "../.."))) +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode +from everest.testing.ocpp_utils.fixtures import * +from ocpp.routing import on, create_route_map +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum) +from ocpp.v201.enums import * +from ocpp.v201.datatypes import * +from ocpp.v201 import call as call201 +from validations import validate_status_notification_201, validate_measurands_match +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +from everest_test_utils import * +from validations import wait_for_callerror_and_validate +# fmt: on + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_F01_F02_F03( + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + F01.FR.01 + F01.FR.02 + F01.FR.03 + F01.FR.05 + F01.FR.07 + F01.FR.14 + F01.FR.19 + F01.FR.23 + """ + + # prepare data for the test + evse_id = 1 + connector_id = 1 + remote_start_id = 1 + id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443) + evse = EVSEType(id=evse_id, connector_id=connector_id) + + # Disable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "false" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14 + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "SampledDataCtrlr", "TxStartedMeasurands" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + expected_started_measurands = get_variables_result.attribute_value.split(",") + + # get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14 + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "SampledDataCtrlr", "TxUpdatedMeasurands" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + + # get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14 + r: call_result201.GetVariablesPayload = ( + await charge_point_v201.get_config_variables_req( + "SampledDataCtrlr", "TxEndedMeasurands" + ) + ) + get_variables_result: GetVariableResultType = GetVariableResultType( + **r.get_variable_result[0] + ) + assert get_variables_result.attribute_status == GetVariableStatusType.accepted + expected_ended_measurands = get_variables_result.attribute_value.split(",") + + # set AuthorizeRemoteStart to true + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "AuthorizeRemoteStart", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # put EVSE to unavailable + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.inoperative, evse=evse + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.unavailable, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + # send RequestStartTransaction while EVSE in unavailable and expect rejected + await charge_point_v201.request_start_transaction_req( + id_token=id_token, remote_start_id=remote_start_id + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.rejected + ), + ) + + # put EVSE to available + await charge_point_v201.change_availablility_req( + operational_status=OperationalStatusType.operative, evse=evse + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + await asyncio.sleep(2) + + # send RequestStartTransaction without evse_id and expect Rejected + await charge_point_v201.request_start_transaction_req( + id_token=id_token, remote_start_id=remote_start_id + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.rejected + ), + ) + + # send RequestStartTransaction and expect Accepted + await charge_point_v201.request_start_transaction_req( + id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.accepted + ), + ) + + # because AuthorizeRemoteStart is true we expect an Authorize here + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call201.AuthorizePayload(id_token=id_token), + ) + + test_controller.plug_in() + # eventType=Started + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"} + ) + test_utility.messages.clear() + test_controller.plug_out() + # eventType=Ended + assert await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + + test_utility.messages.clear() + + # set AuthorizeRemoteStart to false + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "AuthorizeRemoteStart", "false" + ) + ) + test_utility.forbidden_actions.append("Authorize") + + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.occupied, + evse_id, + connector_id, + ), + validate_status_notification_201, + ) + + # send RequestStartTransaction and expect Accepted + await charge_point_v201.request_start_transaction_req( + id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.accepted + ), + ) + + # because AuthorizeRemoteStart is false we directly expect a TransactionEvent(eventType=Started) + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started"}, + ) + ) + + transaction = TransactionType(**r.transaction_info) + + # do some basic checks on TransactionEvent + assert r.trigger_reason == TriggerReasonType.remote_start + assert r.event_type == TransactionEventType.started + assert EVSEType(**r.evse) == evse + assert IdTokenType(**r.id_token) == id_token + + # check if the configured measurands are part of the MeterValue of the TransactionEvent + assert validate_measurands_match( + MeterValueType(**r.meter_value[0]), expected_started_measurands + ) + + await asyncio.sleep(2) + + # send RequestStartTransaction and expect Rejected because transaction_id is wrong + await charge_point_v201.request_stop_transaction_req( + transaction_id="wrong_transaction_id" + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStopTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.rejected + ), + ) + + # send RequestStartTransaction and expect Accepted + await charge_point_v201.request_stop_transaction_req( + transaction_id=transaction.transaction_id + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStopTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.accepted + ), + ) + + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"} + ) + ) + + transaction = TransactionType(**r.transaction_info) + + assert r.trigger_reason == TriggerReasonType.remote_stop + assert transaction.stopped_reason == ReasonType.remote + assert transaction.remote_start_id == remote_start_id + + assert validate_measurands_match( + MeterValueType(**r.meter_value[0]), expected_ended_measurands + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_F06( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + F06.FR.03 + F06.FR.04 + F06.FR.05 + F06.FR.05 + F06.FR.06 + F06.FR.07 + F06.FR.08 + F06.FR.09 + F06.FR.10 + F06.FR.11 + F06.FR.12 + F06.FR.17 + """ + + # Skipped for now (Do test NotImplemented): + # LogStatusNotification + # FirmwareStatusNotification + # PublishFirmwareStatusNotification + # SignChargingStationCertificate + # SignV2GCertificate + # SignCombinedCertificate + + # Test BootNotification + + @on(Action.BootNotification) + def on_boot_notification_pending(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.now().isoformat(), + interval=5, + status=RegistrationStatusType.rejected, + ) + + @on(Action.BootNotification) + def on_boot_notification_accepted(**kwargs): + return call_result201.BootNotificationPayload( + current_time=datetime.utcnow().isoformat(), + interval=300, + status=RegistrationStatusType.accepted, + ) + + central_system_v201.function_overrides.append( + ("on_boot_notification", on_boot_notification_pending) + ) + + test_controller.start() + charge_point_v201: ChargePoint201 = await central_system_v201.wait_for_chargepoint() + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.boot_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + test_utility.validation_mode = ValidationMode.STRICT + assert await wait_for_and_validate( + test_utility, charge_point_v201, "BootNotification", {"reason": "Triggered"} + ) + + setattr(charge_point_v201, "on_boot_notification", on_boot_notification_accepted) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + + # Trigger again so we respond with accepted + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.boot_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "BootNotification", {"reason": "Triggered"} + ) + test_utility.validation_mode = ValidationMode.EASY + + # F06.FR.17: Reject trigger messages when boot_notification_state is Accepted + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.boot_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.rejected + + # Limit the amount of data in metervalues and transactions + + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AlignedDataCtrlr", "Measurands", MeasurandType.current_import + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SampledDataCtrlr", "TxStartedMeasurands", MeasurandType.power_active_import + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SampledDataCtrlr", "TxUpdatedMeasurands", MeasurandType.power_active_import + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SampledDataCtrlr", "TxEndedMeasurands", MeasurandType.power_active_import + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Test Heartbeat + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.heartbeat + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + test_utility.validation_mode = ValidationMode.STRICT + assert await wait_for_and_validate( + test_utility, charge_point_v201, "Heartbeat", {}, timeout=2 + ) + test_utility.validation_mode = ValidationMode.EASY + + # Test Metervalues + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.meter_values, evse=EVSEType(id=1) + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + def check_meter_value(response): + for meter_value in response.meter_value: + value = MeterValueType(**meter_value) + for sampled_value in value.sampled_value: + value = SampledValueType(**sampled_value) + assert value.measurand == MeasurandType.current_import + assert value.context == ReadingContextType.trigger + + r: call201.MeterValuesPayload = call201.MeterValuesPayload( + **await wait_for_and_validate( + test_utility, charge_point_v201, "MeterValues", {"evseId": 1} + ) + ) + check_meter_value(r) + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.meter_values + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + r: call201.MeterValuesPayload = call201.MeterValuesPayload( + **await wait_for_and_validate( + test_utility, charge_point_v201, "MeterValues", {"evseId": 1} + ) + ) + check_meter_value(r) + r: call201.MeterValuesPayload = call201.MeterValuesPayload( + **await wait_for_and_validate( + test_utility, charge_point_v201, "MeterValues", {"evseId": 2} + ) + ) + check_meter_value(r) + + r = await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.meter_values, evse=EVSEType(id=3) + ) + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v201, "OccurrenceConstraintViolation" + ) + + # Test StatusNotification + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.status_notification, + evse=EVSEType(id=1, connector_id=1), + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.status_notification, + evse=EVSEType(id=1, connector_id=2), + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.rejected + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.status_notification, + evse=EVSEType(id=3, connector_id=1), + ) + ) + assert await wait_for_callerror_and_validate( + test_utility, charge_point_v201, "OccurrenceConstraintViolation" + ) + + # F06.FR.12 + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.status_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.rejected + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.status_notification, + evse=EVSEType(id=1), + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.rejected + + # Test TransactionEvent + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.transaction_event, evse=EVSEType(id=1) + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.rejected + + test_controller.swipe("001", connectors=[1, 2]) + test_controller.plug_in() + + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "evse": {"id": 1}}, + ) + ) + transaction_1: TransactionType = TransactionType(**r.transaction_info) + + test_utility.validation_mode = ValidationMode.STRICT + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.transaction_event, evse=EVSEType(id=1) + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "Trigger", + "transactionInfo": {"transactionId": transaction_1.transaction_id}, + }, + ) + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.transaction_event + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "Trigger", + "transactionInfo": {"transactionId": transaction_1.transaction_id}, + }, + ) + test_utility.validation_mode = ValidationMode.EASY + + test_controller.swipe("002", connectors=[1, 2]) + test_controller.plug_in(connector_id=2) + + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "evse": {"id": 2}}, + ) + ) + transaction_2: TransactionType = TransactionType(**r.transaction_info) + + r: call201.TransactionEventPayload = call201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "ChargingStateChanged", + "transactionInfo": {"transactionId": transaction_2.transaction_id}, + }, + ) + ) + + test_utility.validation_mode = ValidationMode.STRICT + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.transaction_event, evse=EVSEType(id=2) + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "Trigger", + "transactionInfo": {"transactionId": transaction_2.transaction_id}, + }, + ) + + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.transaction_event + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "Trigger", + "transactionInfo": {"transactionId": transaction_1.transaction_id}, + }, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + { + "eventType": "Updated", + "triggerReason": "Trigger", + "transactionInfo": {"transactionId": transaction_2.transaction_id}, + }, + ) + test_utility.validation_mode = ValidationMode.EASY + + test_controller.swipe("001", connectors=[1, 2]) + test_controller.swipe("002", connectors=[1, 2]) + test_controller.plug_out() + test_controller.plug_out(connector_id=2) + + # Test LogStatusNotificaiton + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.log_status_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, charge_point_v201, "LogStatusNotification", {"status": "Idle"} + ) + + # Waiting for the log callback to be implemented in the everest core + # log_param = LogParametersType( + # remote_location="ftp://user:12345@localhost:2121", + # oldest_timestamp=(datetime.utcnow() - timedelta(days=1)).isoformat(), + # latest_timestamp=datetime.utcnow().isoformat() + # ) + + # r: call_result201.TriggerMessagePayload = await charge_point_v201.get_log_req(log=log_param, log_type=LogType.diagnostics_log, request_id=10) + # assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "Uploading"}) + # assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "UploadFailed"}) + + # r: call_result201.TriggerMessagePayload = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerType.log_status_notification) + # assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + # test_utility.validation_mode = ValidationMode.STRICT + # assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "Idle"}) + # test_utility.validation_mode = ValidationMode.EASY + + # Test FirmwareStatusNotification + r: call_result201.TriggerMessagePayload = ( + await charge_point_v201.trigger_message_req( + requested_message=MessageTriggerType.firmware_status_notification + ) + ) + assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "FirmwareStatusNotification", + {"status": "Idle"}, + ) + + # Waiting for the update firmware callback to be implemented in the everest core + # firmware_type = FirmwareType( + # location="ftp://user:12345@localhost:2121", + # retrieve_date_time=(datetime.utcnow() + timedelta(seconds=10)).isoformat() + # ) + + # test_utility.validation_mode = ValidationMode.STRICT + + # r: call_result201.UpdateFirmwarePayload = await charge_point_v201.update_firmware(firmware=firmware_type, request_id=10) + + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadScheduled", "requestId": 10}) + # r: call_result201.TriggerMessagePayload = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerType.firmware_status_notification) + # assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadScheduled", "requestId": 10}) + + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Downloading", "requestId": 10}) + + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Downloading", "requestId": 10}) + + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadFailed", "requestId": 10}) + + # r: call_result201.TriggerMessagePayload = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerType.firmware_status_notification) + # assert TriggerMessageStatusType(r.status) == TriggerMessageStatusType.accepted + # assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Idle"}) + + # test_utility.validation_mode = ValidationMode.EASY diff --git a/tests/ocpp_tests/test_sets/ocpp201/security.py b/tests/ocpp_tests/test_sets/ocpp201/security.py new file mode 100644 index 000000000..ebccf7e7d --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/security.py @@ -0,0 +1,930 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import asyncio +import logging +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any + +import pytest +from OpenSSL import crypto +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives._serialization import PublicFormat, Encoding +from everest.testing.core_utils.controller.test_controller_interface import ( + TestController, +) +from everest.testing.core_utils.everest_core import EverestCore +from everest.testing.core_utils.probe_module import ProbeModule + +from everest_test_utils import OCPPConfigReader, CertificateHelper + +from unittest.mock import call as mock_call, Mock, ANY +from ocpp.v201 import call_result +from ocpp.v201.datatypes import SetVariableResultType +from ocpp.v201.enums import SetVariableStatusType + +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +from everest.testing.ocpp_utils.central_system import CentralSystem +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP201ConfigAdjustment, + OCPP201ConfigVariableIdentifier, +) +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + _OCPP201NetworkConnectionProfileAdjustment, +) + + +log = logging.getLogger("OCPP201Security") + + +@dataclass +class _CertificateSigningTestData: + signed_certificate: str | None = None + csr: str | None = None + signed_certificate_valid: bool = True + csr_accepted: bool = True + + +class _BaseTest: + @staticmethod + async def _wait_for_mock_called(mock, call=None, timeout=2): + async def _await_called(): + while not mock.call_count or (call and call not in mock.mock_calls): + await asyncio.sleep(0.1) + + await asyncio.wait_for(_await_called(), timeout=timeout) + + def _setup_csms_mock(self, csms_mock: Mock, test_data: _CertificateSigningTestData): + status = "Accepted" if test_data.csr_accepted else "Rejected" + csms_mock.on_sign_certificate.side_effect = ( + lambda csr: call_result.SignCertificatePayload(status=status) + ) + + def _get_expected_csr_data( + self, certificate_type: str, ocpp_config_reader: OCPPConfigReader + ): + if certificate_type == "ChargingStationCertificate": + + return { + "certificate_type": "CSMS", + "common": ocpp_config_reader.get_variable( + "InternalCtrlr", "ChargeBoxSerialNumber" + ), + "country": ocpp_config_reader.get_variable( + "ISO15118Ctrlr", "ISO15118CtrlrCountryName" + ), + "organization": ocpp_config_reader.get_variable( + "SecurityCtrlr", "OrganizationName" + ), + "use_tpm": False, + } + else: + return { + "certificate_type": "V2G", + "common": ocpp_config_reader.get_variable( + "InternalCtrlr", "ChargeBoxSerialNumber" + ), + "country": ocpp_config_reader.get_variable( + "ISO15118Ctrlr", "ISO15118CtrlrCountryName" + ), + "organization": ocpp_config_reader.get_variable( + "ISO15118Ctrlr", "ISO15118CtrlrOrganizationName" + ), + "use_tpm": False, + } + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config("everest-config-ocpp201-probe-module.yaml") +@pytest.mark.inject_csms_mock +@pytest.mark.probe_module +@pytest.mark.parametrize( + "skip_implementation", + [ + { + "ProbeModuleSecurity": [ + "generate_certificate_signing_request", + "update_leaf_certificate", + ] + } + ], +) +@pytest.mark.skip("The certificate chains are not properly set up to run these tests") +class TestSecurityOCPPIntegration(_BaseTest): + @dataclass + class _SecurityModuleMocks: + generate_certificate_signing_request: Mock + update_leaf_certificate: Mock + + def _setup_security_module_mocks( + self, probe_module: ProbeModule, test_data: _CertificateSigningTestData + ) -> _SecurityModuleMocks: + security_generate_certificate_signing_request_mock = Mock() + security_generate_certificate_signing_request_mock.side_effect = ( + lambda arg: test_data.csr + ) + probe_module.implement_command( + "ProbeModuleSecurity", + "generate_certificate_signing_request", + security_generate_certificate_signing_request_mock, + ) + + security_update_leaf_certificate_mock = Mock() + security_update_leaf_certificate_mock.side_effect = lambda arg: ( + "Accepted" + if test_data.signed_certificate_valid + else "InvalidCertificateChain" + ) + probe_module.implement_command( + "ProbeModuleSecurity", + "update_leaf_certificate", # installs and verifies + security_update_leaf_certificate_mock, + ) + + return self._SecurityModuleMocks( + generate_certificate_signing_request=security_generate_certificate_signing_request_mock, + update_leaf_certificate=security_update_leaf_certificate_mock, + ) + + @pytest.mark.parametrize( + "certificate_type", ["ChargingStationCertificate", "V2GCertificate"] + ) + async def test_A02_update_charging_station_certificate_by_csms_request( + self, + certificate_type, + probe_module, + ocpp_config_reader, + central_system: CentralSystem, + ): + """A02 use case success behavior.""" + + # Setup Test Data & mocks + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + csr="mock certificate request", signed_certificate="mock signed certificate" + ) + security_mocks = self._setup_security_module_mocks(probe_module, test_data) + self._setup_csms_mock(csms_mock, test_data) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await chargepoint_with_pm.trigger_message_req( + requested_message=f"Sign{certificate_type}" # todo: SignV2GCertificate + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + await self._wait_for_mock_called( + security_mocks.generate_certificate_signing_request + ) + assert security_mocks.generate_certificate_signing_request.mock_calls == [ + mock_call(self._get_expected_csr_data(certificate_type, ocpp_config_reader)) + ] + # + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + assert csms_mock.on_sign_certificate.mock_calls == [ + mock_call(csr=test_data.csr) + ] + + # Act II: CSMS sends signed result to ChargePoint + signed_result: call_result.CertificateSignedPayload = ( + await chargepoint_with_pm.certificate_signed_req( + certificate_chain=test_data.signed_certificate, + certificate_type=certificate_type, + ) + ) + + # Verify II + # - Chargepoint accepts signed certificate + # - OCPP module verifies and installs certificate in Security Module + assert signed_result == call_result.CertificateSignedPayload(status="Accepted") + await self._wait_for_mock_called(security_mocks.update_leaf_certificate) + assert security_mocks.update_leaf_certificate.mock_calls == [ + mock_call( + { + "certificate_chain": test_data.signed_certificate, + "certificate_type": ( + "CSMS" + if certificate_type == "ChargingStationCertificate" + else "V2G" + ), + } + ) + ] + + async def test_A02_update_charging_station_certificate_by_csms_request_retry( + self, probe_module, ocpp_config_reader, central_system: CentralSystem + ): + """Test the retry behavior on failed attempts. + + In particular tests requirements A02.FR.17, A02.FR.18, A02.FR.19""" + + # Setup Test Data & mocks + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + csr="mock certificate request", signed_certificate="mock signed certificate" + ) + certificate_type = "ChargingStationCertificate" + security_mocks = self._setup_security_module_mocks(probe_module, test_data) + self._setup_csms_mock(csms_mock, test_data) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await chargepoint_with_pm.trigger_message_req( + requested_message="SignChargingStationCertificate" + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + await self._wait_for_mock_called( + security_mocks.generate_certificate_signing_request + ) + assert security_mocks.generate_certificate_signing_request.mock_calls == [ + mock_call(self._get_expected_csr_data(certificate_type, ocpp_config_reader)) + ] + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + assert csms_mock.on_sign_certificate.mock_calls == [ + mock_call(csr=test_data.csr) + ] + + # Verify: The CSMS does not send the certificate, thus request is repeated as many times as configured + + repeat_times = ocpp_config_reader.get_variable( + "SecurityCtrlr", "CertSigningRepeatTimes" + ) + + async def _await_called(): + while not len(csms_mock.on_sign_certificate.mock_calls) == repeat_times: + await asyncio.sleep(0.1) + + await asyncio.wait_for(_await_called(), 10) + await asyncio.sleep(0.1) # await unexpected further call + + assert csms_mock.on_sign_certificate.mock_calls == repeat_times * [ + mock_call(csr=test_data.csr) + ] + security_mocks.update_leaf_certificate.assert_not_called() + + async def test_A04_rejected_security_event_notification( + self, probe_module, ocpp_config_reader, central_system: CentralSystem + ): + """A02 & A04: OCPP module sends security event if certificate is rejected + + Also tests A02.FR.20 (no repetition) + """ + + # Setup Test Data & mocks + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + csr="mock certificate request", + signed_certificate="mock signed certificate", + signed_certificate_valid=False, + ) + certificate_type = "ChargingStationCertificate" + security_mocks = self._setup_security_module_mocks(probe_module, test_data) + self._setup_csms_mock(csms_mock, test_data) + + # start and ready probe module EvseManagers and wait for libocpp to connect + probe_module.start() + await probe_module.wait_to_be_ready() + probe_module.publish_variable("ProbeModuleConnectorA", "ready", True) + probe_module.publish_variable("ProbeModuleConnectorB", "ready", True) + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await chargepoint_with_pm.trigger_message_req( + requested_message="SignChargingStationCertificate" # todo: SignV2GCertificate + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + await self._wait_for_mock_called( + security_mocks.generate_certificate_signing_request + ) + assert security_mocks.generate_certificate_signing_request.mock_calls == [ + mock_call(self._get_expected_csr_data(certificate_type, ocpp_config_reader)) + ] + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + assert csms_mock.on_sign_certificate.mock_calls == [ + mock_call(csr=test_data.csr) + ] + + # Act II: CSMS sends signed result to ChargePoint + signed_result: call_result.CertificateSignedPayload = ( + await chargepoint_with_pm.certificate_signed_req( + certificate_chain=test_data.signed_certificate, + certificate_type=certificate_type, + ) + ) + # Verify II + # - Chargepoint accepts signed certificate + # - OCPP module rejects certificate and sends security event notification + assert signed_result == call_result.CertificateSignedPayload(status="Rejected") + await self._wait_for_mock_called( + csms_mock.on_security_event_notification, + call=mock_call( + timestamp=ANY, + tech_info="InvalidCertificateChain", + type="InvalidChargingStationCertificate", + ), + ) + + # test A02.FR.20 - no repeated request + await asyncio.sleep( + 0.3 + ) # wait the minimum time between two retries and a little more + assert csms_mock.on_sign_certificate.call_count == 1 + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.everest_core_config("everest-config-ocpp201.yaml") +@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs") +@pytest.mark.inject_csms_mock +@pytest.mark.skip("The certificate chains are not properly set up to run these tests") +class TestSecurityOCPPE2E(_BaseTest): + @dataclass + class _ParsedCSR: + csr: str + common: str + organization: str + country: str + email_address: str | None + public_key: str + + @classmethod + def _parse_certificate_request(cls, csr: str) -> _ParsedCSR: + request = x509.load_pem_x509_csr(csr.encode("utf-8"), default_backend()) + email_address = None + if request.subject.get_attributes_for_oid(x509.NameOID.EMAIL_ADDRESS): + email_address = request.subject.get_attributes_for_oid( + x509.NameOID.EMAIL_ADDRESS + )[0].value + return cls._ParsedCSR( + csr, + request.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value, + request.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)[ + 0 + ].value, + request.subject.get_attributes_for_oid(x509.NameOID.COUNTRY_NAME)[0].value, + email_address, + request.public_key() + .public_bytes( + encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo + ) + .decode("utf-8"), + ) + + @pytest.mark.parametrize( + "certificate_type", ["ChargingStationCertificate", "V2GCertificate"] + ) + async def test_A02_update_charging_station_certificate_by_csms_request( + self, + certificate_type, + ocpp_config_reader, + central_system: CentralSystem, + charge_point: ChargePoint201, + ): + """A02 use case success behavior (installation of new certificate) + + Tested Requirements: A02.FR.02, A02.FR.05 (as far as possible in this context), A02.FR.06 + """ + + # Setup Test Data & mocks + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + signed_certificate="mock signed certificate" + ) + + self._setup_csms_mock(csms_mock, test_data) + + # # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await charge_point.trigger_message_req( + requested_message=f"Sign{certificate_type}" + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + assert csms_mock.on_sign_certificate.mock_calls == [ + mock_call(csr=ANY, certificate_type=certificate_type) + ] + received_csr_data = self._parse_certificate_request( + csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"] + ) + expected_csr_data = self._get_expected_csr_data( + certificate_type, ocpp_config_reader + ) + assert received_csr_data.common == expected_csr_data["common"] + assert received_csr_data.organization == expected_csr_data["organization"] + assert received_csr_data.country == expected_csr_data["country"] + + if certificate_type == "ChargingStationCertificate": + signed_certificate = CertificateHelper.sign_certificate_request( + received_csr_data.csr, + issuer_certificate_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem", + issuer_private_key_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key", + issuer_private_key_passphrase="123456", + ) + else: + # build certificate chain starting with leaf up to CPO SUB CA1 + signed_certificate = CertificateHelper.sign_certificate_request( + received_csr_data.csr, + issuer_certificate_path=Path(__file__).parent + / "../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem", + issuer_private_key_path=Path(__file__).parent + / "../everest-aux/certs/client/csms/CPO_SUB_CA2.key", + issuer_private_key_passphrase="123456", + ) + signed_certificate += ( + Path(__file__).parent / "../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem" + ).read_text() + "\n" + signed_certificate += ( + Path(__file__).parent / "../everest-aux/certs/ca/cso/CPO_SUB_CA1.pem" + ).read_text() + "\n" + + # Act II: CSMS sends signed result to ChargePoint + signed_result: call_result.CertificateSignedPayload = ( + await charge_point.certificate_signed_req( + certificate_chain=signed_certificate, certificate_type=certificate_type + ) + ) + + # Verify II + # - Chargepoint accepts signed certificate + # - OCPP module verifies and installs certificate in Security Module + assert signed_result == call_result.CertificateSignedPayload(status="Accepted") + + async def test_A02_update_charging_station_certificate_by_csms_request_invalid_as_expired( + self, + ocpp_config_reader, + central_system: CentralSystem, + charge_point: ChargePoint201, + ): + """Test the charging station rejects an expired certificate after a signing request.""" + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + signed_certificate="mock signed certificate" + ) + + self._setup_csms_mock(csms_mock, test_data) + + # # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await charge_point.trigger_message_req( + requested_message=f"SignChargingStationCertificate" + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + received_csr_data = self._parse_certificate_request( + csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"] + ) + signed_certificate = CertificateHelper.sign_certificate_request( + received_csr_data.csr, + issuer_certificate_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem", + issuer_private_key_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key", + issuer_private_key_passphrase="123456", + relative_expiration_time=-60, # expired a minute ago + ) + + # Act II: CSMS sends signed result to ChargePoint + signed_result: call_result.CertificateSignedPayload = ( + await charge_point.certificate_signed_req( + certificate_chain=signed_certificate, + certificate_type="ChargingStationCertificate", + ) + ) + + # Verify II + # - Chargepoint accepts signed certificate + # - OCPP module rejects wrongly signed certificate + assert signed_result == call_result.CertificateSignedPayload(status="Rejected") + + # Assert an InvalidChargingStationCertificate event is triggered + await self._wait_for_mock_called( + csms_mock.on_security_event_notification, + call=mock_call( + timestamp=ANY, + tech_info="Expired", + type="InvalidChargingStationCertificate", + ), + ) + + async def test_A02_update_charging_station_certificate_by_csms_request_invalid_due_to_wrong_ca( + self, + ocpp_config_reader, + central_system: CentralSystem, + charge_point: ChargePoint201, + ): + csms_mock = central_system.mock + test_data = _CertificateSigningTestData( + signed_certificate="mock signed certificate" + ) + + self._setup_csms_mock(csms_mock, test_data) + + # # Act CSMS triggers SignChargingStationCertificate + trigger_result: call_result.TriggerMessagePayload = ( + await charge_point.trigger_message_req( + requested_message=f"SignChargingStationCertificate" + ) + ) + + # Verify: + # - OCPP accepts trigger and + # - calls security module "generate_certificate_signing_request" + # - sends CSR to CSMS + assert trigger_result.status == "Accepted" + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + received_csr_data = self._parse_certificate_request( + csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"] + ) + # the MO root ca is invalid for the CSMS certificate! + signed_certificate = CertificateHelper.sign_certificate_request( + received_csr_data.csr, + issuer_certificate_path=Path(__file__).parent + / "../everest-aux/certs/ca/mo/MO_ROOT_CA.pem", + issuer_private_key_path=Path(__file__).parent + / "../everest-aux/certs/client/mo/MO_ROOT_CA.key", + issuer_private_key_passphrase="123456", + ) + + # Act II: CSMS sends signed result to ChargePoint + signed_result: call_result.CertificateSignedPayload = ( + await charge_point.certificate_signed_req( + certificate_chain=signed_certificate, + certificate_type="ChargingStationCertificate", + ) + ) + + # Verify II + # - Chargepoint accepts signed certificate + # - OCPP module rejects wrongly signed certificate + assert signed_result == call_result.CertificateSignedPayload(status="Rejected") + + # Assert an InvalidChargingStationCertificate event is triggered + await self._wait_for_mock_called( + csms_mock.on_security_event_notification, + call=mock_call( + timestamp=ANY, + tech_info="InvalidCertificateChain", + type="InvalidChargingStationCertificate", + ), + ) + + def assert_websocket_client_sslproto_certificate_equals_certificate( + self, websocket_client_cert: dict[str, Any], certificate: str + ): + x509_cert = crypto.load_certificate( + crypto.FILETYPE_PEM, certificate.encode("utf-8") + ) + + def _compare_websocket_and_cert_components( + websocket_components, cert_components + ): + websocket_cert_subject_dict = {k: v for ((k, v),) in websocket_components} + cert_subject_dict = {k: v for (k, v) in cert_components} + for websocket_key, cert_key in [ + ("countryName", b"C"), + ("commonName", b"CN"), + ("organizationName", b"O"), + ("domainComponent", b"DC"), + ]: + assert websocket_cert_subject_dict.get( + websocket_key, "" + ) == cert_subject_dict.get(cert_key, b"").decode("utf-8") + + _compare_websocket_and_cert_components( + websocket_client_cert["subject"], x509_cert.get_subject().get_components() + ) + _compare_websocket_and_cert_components( + websocket_client_cert["issuer"], x509_cert.get_issuer().get_components() + ) + assert ( + int(websocket_client_cert["serialNumber"], 16) + == x509_cert.get_serial_number() + ) + assert datetime.strptime( + websocket_client_cert["notBefore"], "%b %d %H:%M:%S %Y GMT" + ) == datetime.strptime( + x509_cert.get_notBefore().decode("utf-8"), "%Y%m%d%H%M%SZ" + ) + + @pytest.mark.csms_tls(verify_client_certificate=True) + @pytest.mark.ocpp_config_adaptions( + _OCPP201NetworkConnectionProfileAdjustment(None, None, 3) + ) + async def test_A02_use_newest_certificate_after_installation( + self, central_system: CentralSystem, charge_point: ChargePoint201 + ): + """Test station uses new certificate after installation + + Tests requirement A02.FR.08 + """ + + # Check originally used certificate + assert len(central_system.ws_server.websockets) == 1 + old_connection = next(iter(central_system.ws_server.websockets)) + original_certificate = old_connection.transport.get_extra_info("peercert") + expected_original_certificate = ( + Path(__file__).parent / "../everest-aux/certs/client/csms/CSMS_RSA.pem" + ).read_text() + self.assert_websocket_client_sslproto_certificate_equals_certificate( + original_certificate, expected_original_certificate + ) + + # Install new certificate by CSMS request + csms_mock = central_system.mock + self._setup_csms_mock(csms_mock, _CertificateSigningTestData()) + + await charge_point.trigger_message_req( + requested_message=f"SignChargingStationCertificate" + ) + + await self._wait_for_mock_called(csms_mock.on_sign_certificate) + + received_csr_data = self._parse_certificate_request( + csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"] + ) + signed_certificate = CertificateHelper.sign_certificate_request( + received_csr_data.csr, + issuer_certificate_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem", + issuer_private_key_path=Path(__file__).parent + / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key", + issuer_private_key_passphrase="123456", + ) + signed_result: call_result.CertificateSignedPayload = ( + await charge_point.certificate_signed_req( + certificate_chain=signed_certificate, + certificate_type="ChargingStationCertificate", + ) + ) + assert signed_result == call_result.CertificateSignedPayload(status="Accepted") + + # Verify: wait for new connection to be established; validate certificate + async def wait_for_reconnect(): + while len( + central_system.ws_server.websockets + ) < 1 or central_system.ws_server.websockets == {old_connection}: + await asyncio.sleep(0.1) + + await asyncio.wait_for(wait_for_reconnect(), 4) + assert len(central_system.ws_server.websockets) == 1 + new_connection = next(iter(central_system.ws_server.websockets)) + new_certificate = new_connection.transport.get_extra_info("peercert") + self.assert_websocket_client_sslproto_certificate_equals_certificate( + new_certificate, signed_certificate + ) + + @pytest.mark.ocpp_config_adaptions( + _OCPP201NetworkConnectionProfileAdjustment(None, None, 3) + ) + @pytest.mark.csms_tls(verify_client_certificate=True) + async def test_A02_use_newest_certificate_according_to_validity( + self, + everest_core: EverestCore, + test_controller: TestController, + central_system: CentralSystem, + charge_point: ChargePoint201, + ): + """Verifies condition A02.FR.09: The Charging Station SHALL use the newest certificate, as measured by the start of the validity period.""" + + assert len(central_system.ws_server.websockets) == 1 + old_connection = next(iter(central_system.ws_server.websockets)) + + # Setup: install new certificates + cert_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["csms_leaf_cert_directory"] + ) + key_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["csms_leaf_key_directory"] + ) + + ca_certificate = ( + Path(__file__).parent / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem" + ) + ca_key = Path(__file__).parent / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key" + ca_passphrase = "123456" # nosec bandit B105 + + # Install 3 certificates; the second is newest w.r.t. validity (shortest relative shift from now to the past) + certificates = {} + for cert_index, (cert_name, relative_valid_time) in enumerate( + [("Cert1-notNewest", -50), ("Cert2-newest", -10), ("Cert3-notNewest", -100)] + ): + cert_req, cert_key = CertificateHelper.generate_certificate_request( + cert_name + ) + cert = CertificateHelper.sign_certificate_request( + cert_req, + issuer_certificate_path=ca_certificate, + issuer_private_key_path=ca_key, + issuer_private_key_passphrase=ca_passphrase, + serial=42 + cert_index, + relative_valid_time=relative_valid_time, + ) + (cert_directory / f"{cert_name}.pem").write_text(cert) + (key_directory / f"{cert_name}.key").write_text(cert_key) + certificates[cert_name] = cert + test_controller.stop() + test_controller.start() + + async def wait_for_reconnect(): + while len( + central_system.ws_server.websockets + ) < 1 or central_system.ws_server.websockets == {old_connection}: + await asyncio.sleep(0.1) + + await asyncio.wait_for(wait_for_reconnect(), 5) + assert len(central_system.ws_server.websockets) == 1 + + new_connection = next(iter(central_system.ws_server.websockets)) + new_certificate = new_connection.transport.get_extra_info("peercert") + self.assert_websocket_client_sslproto_certificate_equals_certificate( + new_certificate, certificates["Cert2-newest"] + ) + + @pytest.mark.parametrize("certificate_type", ["ChargingStation", "V2G"]) + @pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "InternalCtrlr", + "V2GCertificateExpireCheckInitialDelaySeconds", + "Actual", + ), + 0, + ) + ] + ) + ) + @pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "InternalCtrlr", + "ClientCertificateExpireCheckInitialDelaySeconds", + "Actual", + ), + 0, + ) + ] + ) + ) + @pytest.mark.ocpp_config_adaptions( + _OCPP201NetworkConnectionProfileAdjustment(None, None, 3) + ) + async def test_A03_install_new_if_expired( + self, + certificate_type, + everest_core: EverestCore, + test_controller: TestController, + central_system: CentralSystem, + charge_point: ChargePoint201, + ): + """Verifies condition A03.FR.02: Expiring certificates shall be renewed.""" + + csms_mock = central_system.mock + self._setup_csms_mock(csms_mock, _CertificateSigningTestData()) + + # Setup: install new certificates that shortly expire + if certificate_type == "ChargingStation": + cert_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["csms_leaf_cert_directory"] + ) + key_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["csms_leaf_key_directory"] + ) + + ca_certificate = ( + Path(__file__).parent / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem" + ) + ca_key = ( + Path(__file__).parent / "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key" + ) + ca_passphrase = "123456" # nosec bandit B10 + else: + cert_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["secc_leaf_cert_directory"] + ) + key_directory = Path( + everest_core.everest_config["active_modules"]["evse_security"][ + "config_module" + ]["secc_leaf_cert_directory"] + ) + + ca_certificate = ( + Path(__file__).parent / "../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem" + ) + ca_key = ( + Path(__file__).parent + / "../everest-aux/certs/client/csms/CPO_SUB_CA2.key" + ) + ca_passphrase = "123456" # nosec bandit B105 + + # Remove old certificates + + for f in cert_directory.glob("*.pem"): + f.unlink() + for f in cert_directory.glob("*.key"): + f.unlink() + + # Install new one almost expired + + cert_req, cert_key = CertificateHelper.generate_certificate_request( + "almost expired" + ) + cert = CertificateHelper.sign_certificate_request( + cert_req, + issuer_certificate_path=ca_certificate, + issuer_private_key_path=ca_key, + issuer_private_key_passphrase=ca_passphrase, + relative_expiration_time=300, # expires in 5 minutes + ) + (cert_directory / "almost_expired.pem").write_text(cert) + (key_directory / "almost_expired.key").write_text(cert_key) + + test_controller.stop() + test_controller.start() + + await self._wait_for_mock_called(csms_mock.on_sign_certificate, timeout=10) + + async def test_A01( + self, central_system: CentralSystem, charge_point_v201: ChargePoint201 + ): + # Disable AuthCacheCtrlr + r: call_result.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "SecurityCtrlr", "BasicAuthPassword", "BEEFDEADBEEFDEAD" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # wait for reconnect + await central_system.wait_for_chargepoint(wait_for_bootnotification=False) diff --git a/tests/ocpp_tests/test_sets/ocpp201/transactions.py b/tests/ocpp_tests/test_sets/ocpp201/transactions.py new file mode 100644 index 000000000..021d2b4eb --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/transactions.py @@ -0,0 +1,453 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import pytest +import asyncio +from datetime import datetime + +# fmt: off +import logging + +from everest.testing.core_utils.controller.test_controller_interface import TestController + +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 +from ocpp.v201.enums import * +from ocpp.v201.datatypes import * +from everest.testing.ocpp_utils.fixtures import * +from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum, SetVariableStatusType, ClearCacheStatusType, ConnectorStatusType) +from validations import validate_status_notification_201 +from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP201ConfigAdjustment, OCPP201ConfigVariableIdentifier +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +# fmt: on + +log = logging.getLogger("transactionsTest") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_E04( + central_system_v201: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + E04.FR.01 + ... + """ + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + evse_id2 = 2 + + # make an unknown IdToken + id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443) + + log.info( + "##################### E04: Transaction started while charging station is offline #################" + ) + + test_controller.start() + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + + # Enable AuthCacheCtrlr + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "Enabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable LocalPreAuthorize + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "LocalPreAuthorize", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Set AuthCacheLifeTime + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCacheCtrlr", "LifeTime", "86400" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Clear cache + r: call_result201.ClearCachePayload = await charge_point_v201.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + + # E04.FR.03 the queued transaction messages must contain the flag 'offline' as TRUE + + # Enable offline authorization for unknown ID + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + # Enable AlignedDataSignReadings (Not implemented yet) + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AlignedDataCtrlr", "SignReadings", "true" + ) + ) + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + test_utility.messages.clear() + + # Disconnect CS + log.debug(" Disconnect the CS from the CSMS") + test_controller.disconnect_websocket() + + await asyncio.sleep(2) + + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + + # start charging session + test_controller.plug_in() + + # charge for 30 seconds + await asyncio.sleep(30) + + # swipe id tag to de-authorize + test_controller.swipe(id_token.id_token) + + # stop charging session + test_controller.plug_out() + + await asyncio.sleep(10) + + # Connect CS + log.debug(" Connect the CS to the CSMS") + test_controller.connect_websocket() + + # wait for reconnect + charge_point_v201 = await central_system_v201.wait_for_chargepoint( + wait_for_bootnotification=False + ) + + # All offline generated transaction messaages must be marked offline = True + + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "offline": True}, + ) + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated", "offline": True}, + ) + # should send a Transaction event C15.FR.02 + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended", "offline": True}, + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.inject_csms_mock +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "OCPPCommCtrlr", "MessageTimeout", "Actual" + ), + "1", + ), + ( + OCPP201ConfigVariableIdentifier( + "OCPPCommCtrlr", "MessageAttemptInterval", "Actual" + ), + "1", + ), + ( + OCPP201ConfigVariableIdentifier( + "OCPPCommCtrlr", "MessageAttempts", "Actual" + ), + "3", + ), + ] + ) +) +async def test_cleanup_transaction_events_after_max_attempts_exhausted( + central_system: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test if transaction events are properly cleaned up after the max message attempts + ... + """ + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + evse_id2 = 2 + connector_id2 = 1 + + # make an unknown IdToken + id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443) + + test_controller.start() + charge_point_v201 = await central_system.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id2, + ), + validate_status_notification_201, + ) + + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + + # start charging session + test_controller.plug_in() + + # should send a Transaction event + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "offline": False}, + ) + + assert central_system.mock.on_transaction_event.call_count == 1 + + # return a CALLERROR for the transaction event + central_system.mock.on_transaction_event.side_effect = [NotImplementedError()] + + await asyncio.sleep(10) + + assert ( + central_system.mock.on_transaction_event.call_count == 4 + ) # initial transaction start and 3 attempts for transaction update + central_system.mock.on_transaction_event.reset() + + # respond properly to transaction events again + central_system.mock.on_transaction_event.side_effect = [ + call_result201.TransactionEventPayload() + ] + + # stop charging session + test_controller.plug_out() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended", "offline": False}, + ) + assert ( + central_system.mock.on_transaction_event.call_count == 5 + ) # initial transaction start and 3 attempts for transaction update and transaction end + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + {"evseId": 1, "connectorId": 1, "connectorStatus": "Available"}, + ) + + test_controller.stop() + + test_controller.start() + + # no attempts on delivering the transaction message should be made + await asyncio.sleep(10) + + assert ( + central_system.mock.on_transaction_event.call_count == 5 + ) # initial transaction start and 3 attempts for transaction update and transaction end + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.inject_csms_mock +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "AlignedDataCtrlr", "AlignedDataTxEndedInterval", "Actual" + ), + "5", + ) + ] + ) +) +async def test_two_parallel_transactions( + central_system: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test if two parallel transactions work + ... + """ + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + evse_id2 = 2 + connector_id2 = 1 + + # make an unknown IdToken + id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443) + id_token2 = IdTokenType(id_token="ABAD1DEA", type=IdTokenTypeEnum.iso14443) + + test_controller.start() + charge_point_v201 = await central_system.wait_for_chargepoint( + wait_for_bootnotification=True + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id, + ), + validate_status_notification_201, + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call201.StatusNotificationPayload( + datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id2, + connector_id=connector_id2, + ), + validate_status_notification_201, + ) + + # swipe id tag to authorize + test_controller.swipe(id_token.id_token, connectors=[1]) + + # start charging session + test_controller.plug_in(evse_id1) + + # should send a Transaction event + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "offline": False}, + ) + + # swipe id tag to authorize + test_controller.swipe(id_token2.id_token, connectors=[2]) + + # start charging session + test_controller.plug_in(evse_id2) + + # should send a Transaction event + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "offline": False}, + ) + # let transactions run for a bit + await asyncio.sleep(10) + # # stop charging session + test_controller.plug_out(evse_id1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended", "offline": False}, + ) + test_controller.plug_out(evse_id2) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended", "offline": False}, + ) diff --git a/tests/ocpp_tests/test_sets/test_config.json b/tests/ocpp_tests/test_sets/test_config.json new file mode 100644 index 000000000..bd13dc596 --- /dev/null +++ b/tests/ocpp_tests/test_sets/test_config.json @@ -0,0 +1,30 @@ +{ + "csms_port": 9000, + "charge_point_info": { + "charge_point_id": "cp001", + "charge_point_vendor": "Pionix", + "charge_point_model": "Yeti", + "firmware_version": "0.4" + }, + "authorization_info": { + "emaid": "UKSWI123456789A", + "valid_id_tag_1": "RFID_VALID1", + "valid_id_tag_2": "RFID_VALID2", + "invalid_id_tag": "RFID_INVALID", + "parent_id_tag": "RFID_PARENT", + "invalid_parent_id_tag": "RFID_PARENT_INVALID" + }, + "certificate_info": { + "csms_root_ca": "everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem", + "csms_root_ca_key": "everest-aux/certs/ca/csms/CSMS_ROOT_CA.key", + "csms_root_ca_invalid": "everest-aux/certs/CSMS_RootCA_RSA_invalid.pem", + "csms_cert": "everest-aux/certs/CSMS_SERVER.pem", + "csms_key": "everest-aux/certs/CSMS_SERVER.key", + "csms_passphrase": "123456", + "mf_root_ca": "everest-aux/certs/ca/mf/MF_ROOT_CA.pem" + }, + "firmware_info": { + "update_file": "everest-aux/firmware/firmware_update.pnx", + "update_file_signature": "everest-aux/firmware/firmware_update.pnx.base64" + } +} diff --git a/tests/ocpp_tests/test_sets/validations.py b/tests/ocpp_tests/test_sets/validations.py new file mode 100644 index 000000000..fac332e55 --- /dev/null +++ b/tests/ocpp_tests/test_sets/validations.py @@ -0,0 +1,383 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import json +import logging +import time +import asyncio +from datetime import datetime, timedelta +from dateutil import parser +import OpenSSL.crypto as crypto +from ocpp.messages import unpack + +from ocpp.v16 import call_result +from ocpp.v201.datatypes import MeterValueType, SampledValueType + +from everest.testing.ocpp_utils.charge_point_utils import ValidationMode + +VALID_ID_TAG_1 = "RFID_VALID1" +VALID_ID_TAG_2 = "RFID_VALID2" +INVALID_ID_TAG = "RFID_INVALID" +PARENT_ID_TAG = "PARENT" +STANDARD_TRANSACTION_ID = 1 + + +def validate_standard_start_transaction(meta_data, msg, exp_payload): + + if msg.action != "StartTransaction": + return False + + success = ( + msg.payload["connectorId"] == exp_payload.connector_id + and (msg.payload["idTag"] == exp_payload.id_tag or exp_payload.id_tag == None) + and msg.payload["meterStart"] == exp_payload.meter_start + and "timestamp" in msg.payload + ) + + if success: + return True + elif not success and meta_data.validation_mode == ValidationMode.STRICT: + assert False + else: + return False + + +def validate_standard_stop_transaction(meta_data, msg, exp_payload): + + if msg.action != "StopTransaction": + return False + + success = ( + "meterStop" in msg.payload + and msg.payload["reason"] == exp_payload.reason + and ( + msg.payload["transactionId"] == exp_payload.transaction_id + or msg.payload["transactionId"] == STANDARD_TRANSACTION_ID + ) + ) + + if exp_payload.id_tag != None: + success = success and msg.payload["idTag"] == exp_payload.id_tag + + if exp_payload.transaction_data != None: + success = success and "transactionData" in msg.payload + + if success: + return True + elif not success and meta_data.validation_mode == ValidationMode.STRICT: + assert False + else: + return False + + +def validate_remote_start_stop_transaction(meta_data, msg, exp_payload): + success = msg.payload["status"] == exp_payload.status + if success: + return True + elif not success and meta_data.validation_mode == ValidationMode.STRICT: + assert False + else: + return False + + +def validate_meter_values( + messages, + periodic_measurands, + clock_aligned_measurands, + periodic_interval, + clock_aligned_interval, +): + + periodic_meter_values = [] + clock_aligned_meter_values = [] + for msg in messages: + if ( + msg.payload["meterValue"][0]["sampledValue"][0]["context"] + == "Sample.Periodic" + ): + periodic_meter_values.extend(msg.payload["meterValue"]) + elif ( + msg.payload["meterValue"][0]["sampledValue"][0]["context"] == "Sample.Clock" + ): + clock_aligned_meter_values.extend(msg.payload["meterValue"]) + + validate_interval(periodic_meter_values, periodic_interval) + validate_interval(clock_aligned_meter_values, clock_aligned_interval) + + validate_clock_alignment(clock_aligned_meter_values, clock_aligned_interval) + + validate_measurands(periodic_meter_values, periodic_measurands) + validate_measurands(clock_aligned_meter_values, clock_aligned_measurands) + + return True + + +def validate_clock_alignment(meter_values, interval): + + if interval == 0: + return True + + for meter_value in meter_values: + dt = parser.parse(meter_value["timestamp"]) + diff = (datetime.min - dt.replace(tzinfo=None)) % timedelta(seconds=interval) + if diff.seconds > 2 and diff.minutes == 0 and diff.hours == 0: + return False + return True + + +def validate_interval(meter_values, interval): + if len(meter_values) <= 1: + return True + + i = 0 + while i < len(meter_values) - 1: + x = meter_values[i] + y = meter_values[i + 1] + x_ts = parser.parse(x["timestamp"]).timestamp() + y_ts = parser.parse(y["timestamp"]).timestamp() + diff = y_ts - x_ts + if abs(diff - interval) > 1: + return False + i += 1 + + return True + + +def validate_measurands(meter_values, measurands): + for measurand in measurands: + found = False + for meter_value in meter_values: + for sampled_meter_value in meter_value["sampledValue"]: + if measurand == sampled_meter_value["measurand"]: + found = True + if not found: + return False + return True + + +def dont_validate_meter_values(x, y, z): + return True + + +def dont_validate_sign_certificate(x, y, z): + return True + + +def dont_validate_boot_notification(x, y, z): + return True + + +def validate_composite_schedule( + meta_data, msg, exp_payload: call_result.GetCompositeSchedulePayload +): + return ( + msg.payload["status"] == exp_payload.status + and msg.payload["connectorId"] == exp_payload.connector_id + and msg.payload["chargingSchedule"]["chargingRateUnit"] + == exp_payload.charging_schedule.charging_rate_unit + and validate_duration( + msg.payload["chargingSchedule"]["duration"], + exp_payload.charging_schedule.duration, + ) + and validate_charging_schedule_periods( + msg.payload["chargingSchedule"]["chargingSchedulePeriod"], + exp_payload.charging_schedule.charging_schedule_period, + ) + ) + + +def validate_duration(duration, exp_duration): + return ( + duration == exp_duration + or duration - 2 == exp_duration + or duration + 2 == exp_duration + ) + + +def validate_charging_schedule_periods(periods, exp_periods): + success = len(periods) >= len(exp_periods) + if success: + for i, exp_period in enumerate(exp_periods): + if periods[i]["limit"] != exp_period.limit: + return False + elif ( + periods[i]["startPeriod"] != exp_period.start_period + and periods[i]["startPeriod"] != exp_period.start_period + 1 + and periods[i]["startPeriod"] != exp_period.start_period - 1 + ): + return False + elif ( + exp_period.number_phases is not None + and periods[i]["numberPhases"] != exp_period.number_phases + ): + return False + return True + else: + return False + + +def validate_security_event_notification(meta_data, msg, exp_payload): + return msg.payload["type"] == exp_payload.type + + +def validate_get_log(meta_data, msg, exp_payload): + return msg.payload["status"] == exp_payload.status + + +def validate_boot_notification(meta_data, msg, exp_payload): + return ( + msg.payload["chargeBoxSerialNumber"] == exp_payload.charge_box_serial_number + and msg.payload["chargePointModel"] == exp_payload.charge_point_model + and msg.payload["chargePointVendor"] == exp_payload.charge_point_vendor + ) + + +def validate_status_notification_201(meta_data, msg, exp_payload): + return ( + msg.payload["connectorStatus"] == exp_payload.connector_status + and msg.payload["evseId"] == exp_payload.evse_id + and msg.payload["connectorId"] == exp_payload.connector_id + ) + + +def validate_notify_report_data_201(meta_data, msg, exp_payload): + found_items = 0 + + for payload in exp_payload.report_data: + el = find_report_data(payload, msg.payload["reportData"]) + if el != None: + if ( + msg.payload["requestId"] == exp_payload.request_id + and payload.variable_attribute.type + == el["variableAttribute"][0]["type"] + and payload.variable_attribute.value + == el["variableAttribute"][0]["value"] + ): + found_items += 1 + if found_items == len(exp_payload.report_data): + return True + else: + return False + + +def find_report_data(report_data_element, report_data_list): + for el in report_data_list: + if ( + el["component"]["name"] == report_data_element.component.name + and el["variable"]["name"] == report_data_element.variable.name + ): + # check if evse id has to be checked + if report_data_element.component.evse != None: + if ( + report_data_element.component.evse.id + == el["component"]["evse"]["id"] + ): + return el + # check if variable instance has to be checked + elif report_data_element.variable.instance != None: + if report_data_element.variable.instance == el["variable"]["instance"]: + return el + else: + return el + return None + + +def validate_data_transfer_pnc_get_15118_ev_certificate(meta_data, msg, exp_payload): + return ( + msg.payload["vendorId"] == exp_payload.vendor_id + and msg.payload["messageId"] == exp_payload.message_id + and "action" in msg.payload["data"] + and "exiRequest" in msg.payload["data"] + and "iso15118SchemaVersion" in msg.payload["data"] + ) + + +def validate_data_transfer_sign_certificate(meta_data, msg, exp_payload): + data = json.loads(msg.payload["data"]) + try: + return ( + msg.payload["vendorId"] == exp_payload.vendor_id + and msg.payload["messageId"] == exp_payload.message_id + and "certificateType" in data + and "csr" in data + and data["certificateType"] == "V2GCertificate" + and crypto.load_certificate_request(crypto.FILETYPE_PEM, data["csr"]) + ) + except Exception: + return False + + +async def wait_for_callerror_and_validate( + meta_data, charge_point, exp_payload, validate_payload_func=None, timeout=30 +): + """ + This method waits for a CallError message + """ + + logging.debug(f"Waiting for CallError") + + # check if expected message has been sent already + if ( + meta_data.validation_mode == ValidationMode.EASY + and validate_call_error_against_old_messages(meta_data, exp_payload) + ): + logging.debug( + f"Found correct CallError message with payload {exp_payload} in old messages" + ) + logging.debug("OK!") + return True + + t_timeout = time.time() + timeout + while time.time() < t_timeout: + try: + raw_message = await asyncio.wait_for( + charge_point.wait_for_message(), timeout=timeout + ) + charge_point.message_event.clear() + msg = unpack(raw_message) + if msg.message_type_id == 4: + return validate_call_error(msg, exp_payload) + except asyncio.TimeoutError: + logging.debug("Timeout while waiting for new message") + + logging.info(f"Timeout while waiting for CallError message") + logging.info("This is the message history") + charge_point.message_history.log_history() + return False + + +def validate_call_error(msg, exp_payload): + if msg.message_type_id == 4: + logging.debug("Received CallError") + if msg.error_code == exp_payload: + return True + else: + logging.error( + f'Wrong error code "{msg.error_code}" expected "{exp_payload}"' + ) + return False + return False + + +def validate_call_error_against_old_messages(meta_data, exp_payload): + if meta_data.messages: + for msg in meta_data.messages: + success = validate_call_error(msg, exp_payload) + if success: + meta_data.messages.remove(msg) + return True + return False + + +def validate_transaction_event_started(meta_data, msg, exp_payload): + return msg.payload["eventType"] == exp_payload.event_type + + +def validate_measurands_match(meter_value: MeterValueType, expected_measurands): + reported_measurands = [] + for element in meter_value.sampled_value: + sampled_value: SampledValueType = SampledValueType(**element) + if sampled_value.measurand not in reported_measurands: + reported_measurands.append(sampled_value.measurand) + + return expected_measurands == reported_measurands diff --git a/types/authorization.yaml b/types/authorization.yaml index 05c1a46f2..db548bfdf 100644 --- a/types/authorization.yaml +++ b/types/authorization.yaml @@ -181,6 +181,9 @@ types: items: minimum: 1 type: integer + reservation_id: + description: The reservation id that is used with this validated token. + type: integer SelectionAlgorithm: description: >- The selection algorithm defines the logic to select one connector diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index e28cc88e0..1ad242cb0 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -479,6 +479,14 @@ types: sCEE-7_7: CEE 7/7 16A socket a.k.a Schuko sType2: IEC62196-2 Type 2 socket a.k.a. Mennekes connector sType3: IEC62196-2 Type 2 socket a.k.a. Scame + Other1PhMax16A: Other single phase (domestic) sockets not mentioned above, rated at no more than 16A. CEE7/17, AS3112, + NEMA 5-15, NEMA 5-20, JISC8303, TIS166, SI 32, CPCS-CCC, SEV1011, etc + Other1PhOver16A: Other single phase sockets not mentioned above (over 16A) + Other3Ph: Other 3 phase sockets not mentioned above. NEMA14-30, NEMA14-50. + Pan: Pantograph connector + wInductive: Wireless inductively coupled connection (generic) + wResonant: Wireless resonant coupled connection (generic) + Undetermined: Yet to be determined (e.g. before plugged in) Unknown: Unknown type: string enum: @@ -496,6 +504,13 @@ types: - sCEE_7_7 - sType2 - sType3 + - Other1PhMax16A + - Other1PhOver16A + - Other3Ph + - Pan + - wInductive + - wResonant + - Undetermined - Unknown Evse: description: Type that defines properties of an EVSE including its connectors diff --git a/types/reservation.yaml b/types/reservation.yaml index b15514786..7de755266 100644 --- a/types/reservation.yaml +++ b/types/reservation.yaml @@ -4,10 +4,12 @@ types: description: >- Data of a ReservationResult Accepted: Reservation has been made - Faulted: Reservation has not been made, because connectors or specified connector are in a faulted state - Occupied: Reservation has not been made. All connectors or the specified connector are occupied + Faulted: Reservation has not been made, because evses / connectors or specified evse / connector are in a + faulted state + Occupied: Reservation has not been made. All evses or the specified evse / connector is occupied Rejected: Reservation has not been made. Charge Point is not configured to accept reservations - Unavailable: Reservation has not been made, because connectors or specified connector are in an unavailable state + Unavailable: Reservation has not been made, because evses or specified evse / connector are in an unavailable + state type: string enum: - Accepted @@ -24,6 +26,11 @@ types: - id_token - expiry_time properties: + evse_id: + description: >- + The id of the evse to be reserved. When dismissed means that the reservation is not for a + specific evse + type: integer reservation_id: description: Id of the reservation type: integer @@ -37,6 +44,10 @@ types: parent_id_token: description: parent_id type: string + connector_type: + description: The connector type to make a reservation for + type: string + $ref: /evse_manager#/ConnectorTypeEnum ReservationEndReason: description: >- Reason for reservation end @@ -63,3 +74,58 @@ types: reservation_id: description: reservation_id type: integer + ReservationCheckStatus: + description: >- + The reservation status of an evse id. + NotReserved: There is no reservation for the given evse id. + NotReservedForToken: There is a reservation for the given evse id, but not for this token. + ReservedForOtherToken: Reserved for other token and reservation has no parent token or parent token does not match. + ReservedForOtherTokenAndHasParentToken: There is a reservation for another id token, but the reservation also has + a group id token (which was not given when calling this function). + type: string + enum: + - NotReserved + - ReservedForToken + - ReservedForOtherToken + - ReservedForOtherTokenAndHasParentToken + ReservationCheck: + description: Check for a reserved token + type: object + additionalProperties: false + required: + - id_token + properties: + evse_id: + description: >- + The id of the evse to check. When it is dismissed, it means that the reservation is not for a + specific evse. + type: integer + id_token: + description: The id token to check the reservation for. + type: string + group_id_token: + description: >- + The group / parent id token to check the reservation for. If id_token is set and group_id_token as well, and + id_token is incorrect, the group_id_token will be checked. If that one is correct, there is a reservation + made for this group id token. + type: string + ReservationUpdateStatus: + description: Status of a reservation + type: object + additionalProperties: false + required: + - reservation_id + - reservation_status + properties: + reservation_id: + description: The reservation id + type: integer + reservation_status: + description: The reservation status + type: string + enum: + - Expired + - Removed + - Placed + - Cancelled + - Used