diff --git a/.ci/build-kit/docker/Dockerfile b/.ci/build-kit/docker/Dockerfile index ca655b11d..d5f7564bd 100644 --- a/.ci/build-kit/docker/Dockerfile +++ b/.ci/build-kit/docker/Dockerfile @@ -1,3 +1,12 @@ # syntax=docker/dockerfile:1 ARG BASE_IMAGE_TAG=latest FROM ghcr.io/everest/everest-ci/build-kit-base:${BASE_IMAGE_TAG} + +# Can be used to use an other version of everest-cmake +# ENV EVEREST_CMAKE_PATH=/usr/lib/cmake/everest-cmake +# ENV EVEREST_CMAKE_VERSION= +# RUN rm -rf ${EVEREST_CMAKE_PATH} \ +# && git clone https://github.com/EVerest/everest-cmake.git ${EVEREST_CMAKE_PATH} \ +# && cd ${EVEREST_CMAKE_PATH} \ +# && git checkout ${EVEREST_CMAKE_VERSION} \ +# && rm -r .git diff --git a/.ci/build-kit/scripts/compile.sh b/.ci/build-kit/scripts/compile.sh index ea63ff6e7..bc50e26df 100755 --- a/.ci/build-kit/scripts/compile.sh +++ b/.ci/build-kit/scripts/compile.sh @@ -1,7 +1,5 @@ #!/bin/sh -set -e - cmake \ -B "$EXT_MOUNT/build" \ -S "$EXT_MOUNT/source" \ @@ -9,8 +7,18 @@ cmake \ -DEVC_ENABLE_CCACHE=1 \ -DISO15118_2_GENERATE_AND_INSTALL_CERTIFICATES=OFF \ -DCMAKE_INSTALL_PREFIX="$EXT_MOUNT/dist" \ - -DWHEEL_INSTALL_PREFIX="$EXT_MOUNT/dist-wheels" \ + -DWHEEL_INSTALL_PREFIX="$EXT_MOUNT/wheels" \ -DBUILD_TESTING=ON \ -DEVEREST_ENABLE_COMPILE_WARNINGS=ON +retVal=$? +if [ $retVal -ne 0 ]; then + echo "Configuring failed with return code $retVal" + exit $retVal +fi -ninja -j$(nproc) -C "$EXT_MOUNT/build" +ninja -C "$EXT_MOUNT/build" +retVal=$? +if [ $retVal -ne 0 ]; then + echo "Compiling failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/create_integration_image.sh b/.ci/build-kit/scripts/create_integration_image.sh new file mode 100755 index 000000000..042f7f6e1 --- /dev/null +++ b/.ci/build-kit/scripts/create_integration_image.sh @@ -0,0 +1,20 @@ +#!/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 \ + pytest-html +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Failed to pip-install" + exit $retVal +fi 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/build-kit/scripts/install.sh b/.ci/build-kit/scripts/install.sh index 5b7888756..174dbc773 100755 --- a/.ci/build-kit/scripts/install.sh +++ b/.ci/build-kit/scripts/install.sh @@ -1,8 +1,9 @@ #!/bin/sh -set -e - ninja -C "$EXT_MOUNT/build" install -ninja -C "$EXT_MOUNT/build" everestpy_install_wheel -ninja -C "$EXT_MOUNT/build" everest-testing_install_wheel -ninja -C "$EXT_MOUNT/build" iso15118_install_wheel +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Installation failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/install_wheels.sh b/.ci/build-kit/scripts/install_wheels.sh new file mode 100755 index 000000000..93b3403b9 --- /dev/null +++ b/.ci/build-kit/scripts/install_wheels.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" \ + everestpy_install_wheel \ + everest-testing_install_wheel \ + iso15118_install_wheel +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Wheel Installation failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/prepare_integration_tests.sh b/.ci/build-kit/scripts/prepare_integration_tests.sh deleted file mode 100755 index dc3ba3115..000000000 --- a/.ci/build-kit/scripts/prepare_integration_tests.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh - -set -e - -rsync -a "$EXT_MOUNT/source/tests" ./ - -pip install --break-system-packages \ - $EXT_MOUNT/wheels/everestpy-*.whl -pip install --break-system-packages \ - $EXT_MOUNT/wheels/everest_testing-*.whl -pip install --break-system-packages \ - pytest-html diff --git a/.ci/build-kit/scripts/run_unit_tests.sh b/.ci/build-kit/scripts/run_unit_tests.sh index d0ef68954..586bcde7b 100755 --- a/.ci/build-kit/scripts/run_unit_tests.sh +++ b/.ci/build-kit/scripts/run_unit_tests.sh @@ -1,7 +1,14 @@ #!/bin/sh -set -e +ninja -C "$EXT_MOUNT/build" test +retVal=$? -trap "cp $EXT_MOUNT/build/Testing/Temporary/LastTest.log $EXT_MOUNT/ctest-report" EXIT +# Copy the LastTest.log file to the mounted directory in any case +cp "$EXT_MOUNT/build/Testing/Temporary/LastTest.log" "$EXT_MOUNT/ctest-report" -ninja -C "$EXT_MOUNT/build" test +if [ $retVal -ne 0 ]; then + echo "Unit tests failed with return code $retVal" + exit $retVal +fi + +set -e diff --git a/.ci/e2e/scripts/run_integration_tests.sh b/.ci/e2e/scripts/run_integration_tests.sh index 0ffbe3410..617949925 100755 --- a/.ci/e2e/scripts/run_integration_tests.sh +++ b/.ci/e2e/scripts/run_integration_tests.sh @@ -1,7 +1,5 @@ #!/bin/sh -set -e - cd tests pytest \ -rA \ @@ -11,3 +9,9 @@ pytest \ core_tests/*.py \ framework_tests/*.py \ --everest-prefix "$EXT_MOUNT/dist" +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Integration tests failed with return code $retVal" + exit $retVal +fi 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/CODEOWNERS b/.github/CODEOWNERS index 696a567f5..86bba89ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -22,26 +22,28 @@ /modules/EnergyNode/ @corneliusclaussen @hikinggrass @pietfried /modules/EvseManager/ @corneliusclaussen @SebaLukas @hikinggrass @pietfried /modules/EvManager/ @SebaLukas @pietfried @MarzellT +/modules/Evse15118D20/ @SebaLukas @a-w50 @corneliusclaussen /modules/EvseSecurity/ @AssemblyJohn @pietfried @hikinggrass /modules/EvseV2G/ @corneliusclaussen @SebaLukas @james-ctc /modules/EvseSlac/ @a-w50 @corneliusclaussen @SebaLukas /modules/OCPP/ @hikinggrass @pietfried @maaikez /modules/OCPP201/ @hikinggrass @pietfried @maaikez /modules/PacketSniffer @corneliusclaussen @SebaLukas @hikinggrass +/modules/PhyVersoBSP @pietfried @hikinggrass @corneliusclaussen @dorezyuk @rckstrh /modules/PyEvJosev @SebaLukas @corneliusclaussen @pietfried /modules/Setup @hikinggrass @corneliusclaussen @pietfried /modules/YetiDriver @corneliusclaussen @hikinggrass /modules/simulation/ @SebaLukas @pietfried @hikinggrass /modules/SlacSimulator/ @SebaLukas @pietfried @corneliusclaussen @MarzellT -/modules/rust_examples/ @SirVer @golovasteek @dorezyuk -**/Cargo.toml @SirVer @golovasteek @dorezyuk -**/Cargo.lock @SirVer @golovasteek @dorezyuk +/modules/rust_examples/ @SirVer @dorezyuk +**/Cargo.toml @SirVer @dorezyuk @pietfried @hikinggrass +**/Cargo.lock @SirVer @dorezyuk @pietfried @hikinggrass # Rust & Bazel -*.rs @SirVer @golovasteek @dorezyuk -*.bazel @SirVer @golovasteek @dorezyuk -*.bzl @SirVer @golovasteek @dorezyuk +*.rs @SirVer @dorezyuk @pietfried @hikinggrass +*.bazel @SirVer @dorezyuk @pietfried @hikinggrass +*.bzl @SirVer @dorezyuk @pietfried @hikinggrass # third-party/bazel -/third-party/bazel/deps_versions.bzl @pietfried @hikinggrass @corneliusclaussen @SebaLukas @a-w50 @SirVer @golovasteek @dorezyuk +/third-party/bazel/deps_versions.bzl @pietfried @hikinggrass @corneliusclaussen @SebaLukas @a-w50 @SirVer @dorezyuk diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 61a701a76..3a6cd3e5a 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -14,196 +14,109 @@ on: schedule: - cron: '37 13,1 * * *' -env: - DOCKER_REGISTRY: ghcr.io - EVEREST_CI_VERSION: v1.3.1 - jobs: - lint: - name: Lint - runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} - steps: - - name: Checkout everest-core - uses: actions/checkout@v4.1.6 - with: - path: source - - name: Run clang-format - uses: everest/everest-ci/github-actions/run-clang-format@v1.3.1 - with: - source-dir: source - extensions: hpp,cpp - exclude: cache - - # Since env variables can't be passed to reusable workflows, we need to pass them as outputs - setup-env: - # This job is currently disabled to allow running ci on PRs from forks - if: false - name: Setup Environment - runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} - outputs: - docker_registry: ${{ env.DOCKER_REGISTRY }} - everest_ci_version: ${{ env.EVEREST_CI_VERSION }} - steps: - - id: check - run: | - echo "Setting up environment" - build-and-push-build-kit: - # This job is currently disabled to allow running ci on PRs from forks - if: false - name: Build and Push Build Kit - uses: everest/everest-ci/.github/workflows/deploy-single-docker-image.yml@v1.3.1 - needs: setup-env + ci: + name: Build, Lint and Test + uses: everest/everest-ci/.github/workflows/continuous_integration.yml@v1.4.2 + permissions: + contents: read secrets: - SA_GITHUB_PAT: ${{ secrets.SA_GITHUB_PAT }} - SA_GITHUB_USERNAME: ${{ secrets.SA_GITHUB_USERNAME }} + coverage_deploy_token: ${{ secrets.SA_GITHUB_PAT }} with: - image_name: ${{ github.event.repository.name }}/build-kit-everest-core - directory: .ci/build-kit/docker - docker_registry: ${{ needs.setup-env.outputs.docker_registry }} - github_ref_before: ${{ github.event.before }} - github_ref_after: ${{ github.event.after }} - platforms: linux/amd64 - depends_on_paths: | - .ci/build-kit - .github/workflows/build_and_test.yaml - build_args: | - BASE_IMAGE_TAG=${{ needs.setup-env.outputs.everest_ci_version }} - - build: - name: Build and Unit Tests - # needs: build-and-push-build-kit - runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} - env: - # Currently the build-kit-base image is used to allow running ci on PRs from forks - # BUILD_KIT_IMAGE: ${{ needs.build-and-push-build-kit.outputs.one_image_tag_long }} - BUILD_KIT_IMAGE: ghcr.io/everest/everest-ci/build-kit-base:v1.3.1 - steps: - - name: Format branch name for cache key - run: | - BRANCH_NAME_FOR_CACHE="${GITHUB_REF_NAME//-/_}" - echo "branch_name_for_cache=${BRANCH_NAME_FOR_CACHE}" >> "$GITHUB_ENV" - - name: Setup cache - uses: actions/cache@v3 - with: - path: cache - key: compile-${{ env.branch_name_for_cache }}-${{ github.sha }} - restore-keys: | - compile-${{ env.branch_name_for_cache }}- - compile- - - name: Checkout everest-core - uses: actions/checkout@v4.1.6 - with: - path: source - - name: Setup run scripts - run: | - mkdir scripts - rsync -a source/.ci/build-kit/scripts/ scripts - - name: Pull build-kit image - run: | - docker pull --quiet ${{ env.BUILD_KIT_IMAGE }} - docker image tag ${{ env.BUILD_KIT_IMAGE }} build-kit - - name: Compile - run: | - docker run \ - --volume "$(pwd):/ext" \ - --name compile-container \ - build-kit run-script compile - - name: Commit compile-container - run: | - docker commit compile-container build-image - - name: Run unit tests - run: | - docker run \ - --volume "$(pwd):/ext" \ - --name unit-tests-container \ - build-image run-script run_unit_tests - - name: Create dist - run: | - docker run \ - --volume "$(pwd):/ext" \ - --name install-container \ - build-image run-script install - - name: Tar dist dir and keep permissions - run: | - tar -czf dist.tar.gz dist - - name: Upload dist artifact - uses: actions/upload-artifact@v4.3.3 - with: - path: dist.tar.gz - name: dist - - name: Upload wheels artifact - uses: actions/upload-artifact@v4.3.3 - with: - path: dist-wheels - name: wheels - - name: Archive unit test results - if: always() - uses: actions/upload-artifact@v4.3.3 - with: - name: ctest-report - path: ${{ github.workspace }}/ctest-report - integration-tests: - name: Integration Tests + runner: ${{ inputs.runner || 'ubuntu-22.04' }} + artifact_deploy_target_repo: EVerest/everest.github.io + run_coverage: false + do_not_run_coverage_badge_creation: true + run_install_wheels: true + run_integration_tests: true + ocpp-tests: + name: OCPP Tests needs: - - build - # - build-and-push-build-kit - env: - # Currently the build-kit-base image is used to allow running ci on PRs from forks - # BUILD_KIT_IMAGE: ${{ needs.build-and-push-build-kit.outputs.one_image_tag_long }} - BUILD_KIT_IMAGE: ghcr.io/everest/everest-ci/build-kit-base:v1.3.1 + - ci runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} steps: - name: Download dist dir - uses: actions/download-artifact@v4.1.7 + 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 - uses: actions/download-artifact@v4.1.7 + # if: ${{ inputs.run_install_wheels == 'true' }} + uses: actions/download-artifact@v4.1.8 with: name: wheels path: wheels - - name: Checkout everest-core - uses: actions/checkout@v4.1.6 + - 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: Pull build-kit image + - 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 pull --quiet ${{ env.BUILD_KIT_IMAGE }} - docker image tag ${{ env.BUILD_KIT_IMAGE }} build-kit + docker load -i build-kit.tar + docker image tag ${{ steps.buildkit_tag.outputs.tag }} build-kit - name: Create integration-image run: | docker run \ - --volume "$(pwd):/ext" \ - --name prepare-container \ - build-kit run-script prepare_integration_tests - docker commit prepare-container integration-image - - name: Run integration tests + --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: | - pushd source/.ci/e2e - docker compose run \ + docker compose \ + -f source/.ci/e2e/docker-compose.yaml \ + run \ e2e-test-server \ - run-script run_integration_tests - - name: Upload result & report as artifact - if: always() - uses: actions/upload-artifact@v4.3.3 + 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: | - ${{ github.workspace }}/result.xml - ${{ github.workspace }}/report.html - name: pytest-results - - name: Render result - if: always() - uses: pmeier/pytest-results-action@v0.6.0 + 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: ${{ github.workspace }}/result.xml + 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 44502c3b5..2a266734f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,12 @@ cmake_minimum_required(VERSION 3.16) project(everest-core - VERSION 2024.9.0 + VERSION 2024.11.0 DESCRIPTION "The open operating system for e-mobility charging stations" LANGUAGES CXX C ) -find_package(everest-cmake 0.4 +find_package(everest-cmake 0.5 COMPONENTS bundling PATHS ../everest-cmake ) @@ -102,6 +102,7 @@ else() find_package(everest-modbus REQUIRED) find_package(everest-ocpp REQUIRED) find_package(cbv2g REQUIRED) + find_package(iso15118 REQUIRED) find_package(PalSigslot REQUIRED) @@ -128,12 +129,6 @@ endif() # FIXME (aw): this should be optional add_subdirectory(config) -# install docker related files -install( - DIRECTORY "cmake/assets/docker" - DESTINATION "${CMAKE_INSTALL_DATADIR}/everest" -) - ev_install_project() # configure clang-tidy if requested @@ -174,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/README.md b/README.md index cbeb0cfb0..38300df72 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ # everest-core -This is the main part of EVerest containing the actual charge controller logic included in a large set of modules. +This is the main part of EVerest containing the actual charge controller logic +included in a large set of modules. -All documentation and the issue tracking can be found in our main repository here: https://github.com/EVerest/everest +All documentation and the issue tracking can be found in our main repository +here: -### Prerequisites: +## Prerequisites -#### Hardware recommendations +### Hardware recommendations -It is recommended to have at least 4GB of RAM available to build EVerest. -More CPU cores will optionally boost the build process, while requiring more RAM accordingly. +It is recommended to have at least 4GB of RAM available to build EVerest. More +CPU cores will optionally boost the build process while requiring more RAM +accordingly. -.. note:: - - EVerest can also run on much lower hardware specifications, if needed. - The reason for this is that the module configuration is very much defining - the RAM requirements. About 128 MB flash / RAM should be seen as an absolute - minimum requirement. +> [!NOTE] +> EVerest can also run on much lower hardware specifications if needed. The +> reason for this is that the module configuration is very much defining the RAM +> requirements. About 128 MB flash / RAM should be seen as an absolute minimum +> requirement. Besides these recommendations, a typical EVerest should meet the following minimum requirements: @@ -34,60 +36,87 @@ minimum requirements: * PLC GreenPhy * RFID -#### Ubuntu 22.04 +### Ubuntu 22.04 -> :warning: Ubuntu 20.04 is not supported anymore. Please use Ubuntu 22.04 or newer. +> [!WARNING] +> Ubuntu 20.04 is not supported anymore. Please use Ubuntu 22.04 or newer. ```bash sudo apt update -sudo apt install -y python3-pip python3-venv git rsync wget cmake doxygen graphviz build-essential clang-tidy cppcheck openjdk-17-jdk npm docker docker-compose libboost-all-dev nodejs libssl-dev libsqlite3-dev clang-format curl rfkill libpcap-dev libevent-dev pkg-config libcap-dev +sudo apt install -y python3-pip python3-venv git rsync wget cmake doxygen \ + graphviz build-essential clang-tidy cppcheck openjdk-17-jdk npm docker \ + docker-compose libboost-all-dev nodejs libssl-dev libsqlite3-dev \ + clang-format curl rfkill libpcap-dev libevent-dev pkg-config libcap-dev ``` -#### OpenSuse -```bash +### OpenSuse + +```shell zypper update && zypper install -y sudo shadow zypper install -y --type pattern devel_basis -zypper install -y git rsync wget cmake doxygen graphviz clang-tools cppcheck boost-devel libboost_filesystem-devel libboost_log-devel libboost_program_options-devel libboost_system-devel libboost_thread-devel java-17-openjdk java-17-openjdk-devel nodejs nodejs-devel npm python3-pip gcc-c++ libopenssl-devel sqlite3-devel libpcap-devel libevent-devel libcap-devel +zypper install -y git rsync wget cmake doxygen graphviz clang-tools cppcheck \ + boost-devel libboost_filesystem-devel libboost_log-devel \ + libboost_program_options-devel libboost_system-devel libboost_thread-devel \ + java-17-openjdk java-17-openjdk-devel nodejs nodejs-devel npm python3-pip \ + gcc-c++ libopenssl-devel sqlite3-devel libpcap-devel libevent-devel \ + libcap-devel ``` -#### Fedora 38, 39 & 40 +### Fedora 38, 39 & 40 + ```bash sudo dnf update -sudo dnf install make automake gcc gcc-c++ kernel-devel python3-pip python3-devel git rsync wget cmake doxygen graphviz clang-tools-extra cppcheck java-17-openjdk java-17-openjdk-devel boost-devel nodejs nodejs-devel npm openssl openssl-devel libsqlite3x-devel curl rfkill libpcap-devel libevent-devel libcap-devel +sudo dnf install make automake gcc gcc-c++ kernel-devel python3-pip \ + python3-devel git rsync wget cmake doxygen graphviz clang-tools-extra \ + cppcheck java-17-openjdk java-17-openjdk-devel boost-devel nodejs \ + nodejs-devel npm openssl openssl-devel libsqlite3x-devel curl rfkill \ + libpcap-devel libevent-devel libcap-devel ``` -### Build & Install: +## Build & Install -It is required that you have uploaded your public [ssh key](https://www.atlassian.com/git/tutorials/git-ssh) to [github](https://github.com/settings/keys). +It is required that you have uploaded your public [ssh key](https://www.atlassian.com/git/tutorials/git-ssh) +to [github](https://github.com/settings/keys). -To install the [Everest Dependency Manager](https://github.com/EVerest/everest-dev-environment/blob/main/dependency_manager/README.md), follow these steps: +To install the [Everest Dependency Manager](https://github.com/EVerest/everest-dev-environment/blob/main/dependency_manager/README.md), +follow these steps: Install required python packages: + ```bash python3 -m pip install --upgrade pip setuptools wheel jstyleson jsonschema ``` -Get EDM source files and change into the directory: + +Get EDM source files and change to the directory: + ```bash git clone git@github.com:EVerest/everest-dev-environment.git cd everest-dev-environment/dependency_manager ``` + Install EDM: + ```bash python3 -m pip install . ``` + We need to add */home/USER/.local/bin* and *CPM_SOURCE_CACHE* to *$PATH*: + ```bash export PATH=$PATH:/home/$(whoami)/.local/bin export CPM_SOURCE_CACHE=$HOME/.cache/CPM ``` -Now setup EVerest workspace: +Now setup EVerest workspace: + ```bash cd everest-dev-environment/dependency_manager edm init --workspace ~/checkout/everest-workspace ``` -This sets up a workspace based on the most recent EVerest release. If you want to check out the most recent main you can use the following command: +This sets up a workspace based on the most recent EVerest release. If you want +to check out the most recent main you can use the following command: + ```bash cd everest-dev-environment/dependency_manager edm init main --workspace ~/checkout/everest-workspace @@ -96,6 +125,7 @@ edm init main --workspace ~/checkout/everest-workspace Install [ev-cli](https://github.com/EVerest/everest-utils/tree/main/ev-dev-tools): Change the directory and install ev-cli: + ```bash cd ~/checkout/everest-workspace/everest-utils/ev-dev-tools python3 -m pip install . @@ -108,7 +138,12 @@ cd ~/checkout/everest-workspace/Josev python3 -m pip install -r requirements.txt ``` -For ISO15118 communication including Plug&Charge you need to install the required CA certificates inside [config/certs/ca](config/certs/ca) and client certificates, private keys and password files inside [config/certs/client](config/certs/client/). For an more seamless development experience, these are automatically generated for you, but you can set the ISO15118_2_GENERATE_AND_INSTALL_CERTIFICATES cmake option to OFF to disable this auto-generation for production use. +For ISO15118 communication including Plug&Charge you need to install the +required CA certificates inside [config/certs/ca](config/certs/ca) and client +certificates, private keys and password files inside [config/certs/client](config/certs/client/). +For a more seamless development experience, these are automatically generated +for you, but you can set the ISO15118_2_GENERATE_AND_INSTALL_CERTIFICATES cmake +option to OFF to disable this auto-generation for production use. Now we can build EVerest! @@ -119,57 +154,70 @@ cmake .. make install ``` -(Optional) In case you have more than one CPU core and more RAM availble you can use the following command to significantly speed up the build process: +> [!TIP] +> In case you have more than one CPU core and more RAM available you can use the +> following command to significantly speed up the build process: + ```bash make -j$(nproc) install ``` -*$(nproc)* puts out the core count of your machine, so it is using all available CPU cores! -You can also specify any number of CPU cores you like. - + +*$(nproc)* puts out the core count of your machine, so it uses all available CPU +cores! You can also specify any number of CPU cores you like. + Done! -### Simulation - -In order to test your build of Everest you can simulate the code on your local machine! Check out the different configuration files to run EVerest and the corresponding nodered flows in the [config folder](config/). +## Simulation - Check out this [guide for using EVerest SIL](https://everest.github.io/nightly/tutorials/run_sil/index.html) +In order to test your build of Everest you can simulate the code on your local +machine! Check out the different configuration files to run EVerest and the +corresponding nodered flows in the [config folder](config/). +Check out this [guide for using EVerest SIL](https://everest.github.io/nightly/tutorials/run_sil/index.html) -### Troubleshoot +## Troubleshoot **1. Problem:** "make install" fails with complaining about missing header files. -**Cause:** Most probably your *clang-format* version is older than 11 and *ev-cli* is not able to generate the header files during the build process. +**Cause:** Most probably your *clang-format* version is older than 11 and +*ev-cli* is not able to generate the header files during the build process. + +**Solution:** Install a newer clang-format version and make Ubuntu using the new +version e.g.: -**Solution:** Install newer clang-format version and make Ubuntu using the new version e.g.: ```bash sudo apt install clang-format-12 sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-12 100 ``` + Verify clang-format version: + ```bash clang-format --version Ubuntu clang-format version 12.0.0-3ubuntu1~20.04.4 ``` -To retry building EVerest delete the entire everest-core/**build** folder and recreate it. -Start building EVerest using *cmake ..* and *make install* again. +To retry building EVerest delete the entire everest-core/**build** folder and +recreate it. Start building EVerest using *cmake ..* and *make install* again. **2. Problem:** Build speed is very slow. **Cause:** *cmake* and *make* are only utilizing one CPU core. -**Solution:** use +**Solution:** use + ```bash cmake -j$(nproc) .. ``` -and + +and + ```bash make -j$(nproc) install ``` -to use all available CPU cores. -Be aware that this will need roughly an additional 1-2GB of RAM per core. -Alternatively you can also use any number between 2 and your maximum core count instead of *$(nproc)*. +to use all available CPU cores. Be aware that this will need roughly an +additional 1-2GB of RAM per core. Alternatively, you can also use any number +between 2 and your maximum core count instead of *$(nproc)*. diff --git a/cmake/assets/docker/Dockerfile b/cmake/assets/docker/Dockerfile deleted file mode 100644 index e44d7b0c8..000000000 --- a/cmake/assets/docker/Dockerfile +++ /dev/null @@ -1,5 +0,0 @@ -FROM nodered/node-red:2.2.3 -RUN npm install node-red-dashboard -RUN npm install node-red-contrib-ui-actions -RUN npm install node-red-node-ui-table -RUN npm install node-red-contrib-ui-level diff --git a/cmake/assets/run_nodered_template.sh.in b/cmake/assets/run_nodered_template.sh.in index 58c90fa2c..40a420fca 100755 --- a/cmake/assets/run_nodered_template.sh.in +++ b/cmake/assets/run_nodered_template.sh.in @@ -1,3 +1,17 @@ -cd @EVEREST_DOCKER_DIR@ -docker build -t everest-nodered . -docker run --rm --network host --name everest_nodered --mount type=bind,source=@FLOW_FILE@,target=/data/flows.json everest-nodered +# In the following a volume is created to contain the nodered config, this is done to +# allow starting the nodered container from inside a devcontainer + +# Create docker volume to contain nodered config +docker volume create everest-nodered-config-volume + +# Create temporarily container to copy nodered config into the created volume +docker run --name everest-nodered-config-container -v everest-nodered-config-volume:/data debian:12-slim chown -R 1000:1000 /data + +# Copy nodered config to the created volume with the temporarily created container +docker cp @FLOW_FILE@ everest-nodered-config-container:/data/flows.json + +# Remove temporarily container +docker rm everest-nodered-config-container + +# Start nodered container with the volume mounted to /data +docker run -it --rm --network host --name everest_nodered --mount type=volume,source=everest-nodered-config-volume,target=/data ghcr.io/everest/everest-dev-environment/nodered:docker-images-v0.1.0 diff --git a/cmake/config-run-nodered-script.cmake b/cmake/config-run-nodered-script.cmake index 048413424..83089c935 100644 --- a/cmake/config-run-nodered-script.cmake +++ b/cmake/config-run-nodered-script.cmake @@ -32,9 +32,6 @@ function(generate_nodered_run_script) set(SCRIPT_OUTPUT_FILE "${SCRIPT_OUTPUT_PATH}/nodered-${OPTNS_OUTPUT}.sh") endif() - # other necessary variables - set(EVEREST_DOCKER_DIR "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/everest/docker") - configure_file("${EVEREST_CONFIG_ASSET_DIR}/run_nodered_template.sh.in" ${SCRIPT_OUTPUT_FILE}) endfunction() diff --git a/cmake/ev-cli.cmake b/cmake/ev-cli.cmake index 9a63653a6..b75f38a42 100644 --- a/cmake/ev-cli.cmake +++ b/cmake/ev-cli.cmake @@ -1,12 +1,9 @@ -macro(setup_ev_cli) +function(setup_ev_cli) if(NOT TARGET ev-cli) add_custom_target(ev-cli) endif() - if(${EV_CLI}) - message(FATAL_ERROR "EV_CLI is already defined.") - return() - endif() if(NOT ${${PROJECT_NAME}_USE_PYTHON_VENV}) + message(STATUS "Using system ev-cli instead of installing it in the build venv.") find_program(EV_CLI ev-cli REQUIRED) else() ev_is_python_venv_active( @@ -15,12 +12,26 @@ macro(setup_ev_cli) if(NOT ${IS_PYTHON_VENV_ACTIVE}) message(FATAL_ERROR "Python venv is not active. Please activate the python venv before running this command.") endif() - set(EV_CLI "${${PROJECT_NAME}_PYTHON_VENV_PATH}/bin/ev-cli") - add_dependencies(ev-cli - ev-dev-tools_pip_install_dist + + get_target_property(SOURCE_DIRECTORY ev_pip_package_ev-dev-tools SOURCE_DIRECTORY) + message(STATUS "Installing ev-cli from: ${SOURCE_DIRECTORY}") + ev_pip_install_local( + PACKAGE_NAME "ev-dev-tools" + PACKAGE_SOURCE_DIRECTORY "${SOURCE_DIRECTORY}" ) + unset(EV_CLI CACHE) + find_program(EV_CLI ev-cli HINTS ${EV_ACTIVATE_PYTHON_VENV_PATH_TO_VENV}/bin REQUIRED) + message(STATUS "Using ev-cli from: ${EV_CLI}") endif() -endmacro() + + get_property(EVEREST_REQUIRED_EV_CLI_VERSION + GLOBAL + PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION + ) + require_ev_cli_version(${EVEREST_REQUIRED_EV_CLI_VERSION}) + + set_ev_cli_template_properties() +endfunction() function(require_ev_cli_version EV_CLI_VERSION_REQUIRED) execute_process( @@ -40,3 +51,51 @@ function(require_ev_cli_version EV_CLI_VERSION_REQUIRED) message(FATAL_ERROR "ev-cli version ${EV_CLI_VERSION_REQUIRED} or higher is required. However your ev-cli version is '${EV_CLI_VERSION}'. Please upgrade ev-cli.") endif() endfunction() + +function(set_ev_cli_template_properties) + message(STATUS "Setting template properties for ev-cli target") + get_target_property(EVEREST_SCHEMA_DIR generate_cpp_files EVEREST_SCHEMA_DIR) + + execute_process( + COMMAND ${EV_CLI} interface get-templates --separator=\; --schemas-dir "${EVEREST_SCHEMA_DIR}" + OUTPUT_VARIABLE INTERFACE_TEMPLATES + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE + INTERFACE_TEMPLATES_RESULT + ) + + if(INTERFACE_TEMPLATES_RESULT) + message(FATAL_ERROR "Could not get interface templates from ev-cli.") + endif() + + execute_process( + COMMAND ${EV_CLI} module get-templates --separator=\; --schemas-dir "${EVEREST_SCHEMA_DIR}" + OUTPUT_VARIABLE MODULE_TEMPLATES + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE + MODULE_TEMPLATES_RESULT + ) + + if(MODULE_TEMPLATES_RESULT) + message(FATAL_ERROR "Could not get module loader templates from ev-cli.") + endif() + + execute_process( + COMMAND ${EV_CLI} types get-templates --separator=\; --schemas-dir "${EVEREST_SCHEMA_DIR}" + OUTPUT_VARIABLE TYPES_TEMPLATES + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE + TYPES_TEMPLATES_RESULT + ) + + if(TYPES_TEMPLATES_RESULT) + message(FATAL_ERROR "Could not get module loader templates from ev-cli.") + endif() + + set_target_properties(ev-cli + PROPERTIES + INTERFACE_TEMPLATES "${INTERFACE_TEMPLATES}" + MODULE_TEMPLATES "${MODULE_TEMPLATES}" + TYPES_TEMPLATES "${TYPES_TEMPLATES}" + ) +endfunction() diff --git a/cmake/ev-project-bootstrap.cmake b/cmake/ev-project-bootstrap.cmake index 8730905c4..e9f5ce6db 100644 --- a/cmake/ev-project-bootstrap.cmake +++ b/cmake/ev-project-bootstrap.cmake @@ -1,13 +1,12 @@ +set_property( + GLOBAL + PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION "0.4.0" +) # FIXME (aw): clean up this inclusion chain include(${CMAKE_CURRENT_LIST_DIR}/ev-cli.cmake) include(${CMAKE_CURRENT_LIST_DIR}/config-run-script.cmake) include(${CMAKE_CURRENT_LIST_DIR}/config-run-nodered-script.cmake) -set_property( - GLOBAL - PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION "0.2.0" -) - # source generate scripts / setup include(${CMAKE_CURRENT_LIST_DIR}/everest-generate.cmake) diff --git a/cmake/everest-generate.cmake b/cmake/everest-generate.cmake index c5fe381d3..bbfe81839 100644 --- a/cmake/everest-generate.cmake +++ b/cmake/everest-generate.cmake @@ -135,16 +135,6 @@ macro(ev_add_project) ) setup_ev_cli() - if(NOT ${${PROJECT_NAME}_USE_PYTHON_VENV}) - message(STATUS "Using system ev-cli instead of installing it in the build venv.") - get_property(EVEREST_REQUIRED_EV_CLI_VERSION - GLOBAL - PROPERTY EVEREST_REQUIRED_EV_CLI_VERSION - ) - require_ev_cli_version(${EVEREST_REQUIRED_EV_CLI_VERSION}) - else() - message(STATUS "Installing ev-cli in the build venv.") - endif() # FIXME (aw): resort to proper argument handling! if (${ARGC} EQUAL 2) @@ -313,7 +303,7 @@ function (_ev_add_interfaces) "${CHECK_DONE_FILE}" DEPENDS ${ARGV} - ev-cli + "$" COMMENT "Generating/updating interface files ..." VERBATIM @@ -352,7 +342,7 @@ function (_ev_add_types) "${CHECK_DONE_FILE}" DEPENDS ${ARGV} - ev-cli + "$" COMMENT "Generating/updating type files ..." VERBATIM @@ -496,7 +486,7 @@ function (ev_add_cpp_module MODULE_NAME) ${RELATIVE_MODULE_DIR} DEPENDS ${MODULE_PATH}/manifest.yaml - ev-cli + "$" WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMENT @@ -753,4 +743,4 @@ endfunction() set(EVEREST_EXCLUDE_MODULES "" CACHE STRING "A list of modules that will not be built") set(EVEREST_INCLUDE_MODULES "" CACHE STRING "A list of modules that will be built. If the list is empty, all modules will be built.") -option(EVEREST_EXCLUDE_CPP_MODULES "Exclude all C++ modules from the build" OFF) +option(EVEREST_EXCLUDE_CPP_MODULES "Exclude all C++ modules from the build" OFF) \ No newline at end of file diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index 499a2d2dd..24dbd5060 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -3,7 +3,10 @@ generate_config_run_script(CONFIG sil-two-evse) generate_config_run_script(CONFIG sil-ocpp) generate_config_run_script(CONFIG sil-ocpp201) generate_config_run_script(CONFIG sil-dc) +generate_config_run_script(CONFIG sil-dc-d20) generate_config_run_script(CONFIG sil-dc-tls) +generate_config_run_script(CONFIG sil-dc-isomux) +generate_config_run_script(CONFIG sil-dc-isomux-tls) generate_config_run_script(CONFIG sil-dc-sae-v2g) generate_config_run_script(CONFIG sil-dc-sae-v2h) generate_config_run_script(CONFIG sil-two-evse-dc) diff --git a/config/config-sil-dc-d20.yaml b/config/config-sil-dc-d20.yaml new file mode 100644 index 000000000..7195d547f --- /dev/null +++ b/config/config-sil-dc-d20.yaml @@ -0,0 +1,148 @@ +active_modules: + iso15118_charger: + module: Evse15118D20 + config_module: + device: auto + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: false + supported_ISO15118_2: false + supported_ISO15118_20_DC: true + tls_active: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + payment_enable_contract: false + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: DCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: SlacSimulator + imd: + config_implementation: + main: + selftest_success: true + module: IMDSimulator + ev_manager: + module: EvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: false + auto_exec_commands: sleep 3;iso_wait_slac_matched;iso_start_v2g_session DC;iso_wait_pwr_ready;iso_wait_for_stop 15;iso_wait_v2g_session_stopped;unplug; + dc_target_current: 20 + dc_target_voltage: 400 + 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 + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + 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: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse + error_history: + - module_id: error_history + implementation_id: error_history + error_history: + module: ErrorHistory + config_implementation: + error_history: + database_path: /tmp/error_history.db +x-module-layout: {} diff --git a/config/config-sil-dc-isomux-tls.yaml b/config/config-sil-dc-isomux-tls.yaml new file mode 100644 index 000000000..f590d0543 --- /dev/null +++ b/config/config-sil-dc-isomux-tls.yaml @@ -0,0 +1,177 @@ +active_modules: + iso15118_2: + module: EvseV2G + config_module: + device: lo + tls_security: allow + enable_sdp_server: false + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_20: + module: Evse15118D20 + config_module: + device: lo + tls_negotiation_strategy: ACCEPT_CLIENT_OFFER + enable_sdp_server: false + iso_mux: + module: IsoMux + config_module: + device: auto + tls_security: force + connections: + security: + - module_id: evse_security + implementation_id: main + iso2: + - module_id: iso15118_2 + implementation_id: charger + iso20: + - module_id: iso15118_20 + implementation_id: charger + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: false + supported_ISO15118_2: true + supported_ISO15118_20_DC: true + tls_active: true + enforce_tls: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + hack_allow_bpt_with_iso2: true + payment_enable_contract: false + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso_mux + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: DCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: SlacSimulator + imd: + config_implementation: + main: + selftest_success: true + module: IMDSimulator + ev_manager: + module: EvManager + 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 + dc_target_current: 20 + dc_target_voltage: 400 + 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 + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + 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: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse + error_history: + - module_id: error_history + implementation_id: error_history + error_history: + module: ErrorHistory + config_implementation: + error_history: + database_path: /tmp/error_history.db +x-module-layout: {} diff --git a/config/config-sil-dc-isomux.yaml b/config/config-sil-dc-isomux.yaml new file mode 100644 index 000000000..a51816050 --- /dev/null +++ b/config/config-sil-dc-isomux.yaml @@ -0,0 +1,174 @@ +active_modules: + iso15118_2: + module: EvseV2G + config_module: + device: lo + tls_security: allow + enable_sdp_server: false + connections: + security: + - module_id: evse_security + implementation_id: main + iso15118_20: + module: Evse15118D20 + config_module: + device: lo + tls_negotiation_strategy: ACCEPT_CLIENT_OFFER + enable_sdp_server: false + iso_mux: + module: IsoMux + config_module: + device: auto + tls_security: allow + connections: + security: + - module_id: evse_security + implementation_id: main + iso2: + - module_id: iso15118_2 + implementation_id: charger + iso20: + - module_id: iso15118_20 + implementation_id: charger + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: true + supported_ISO15118_2: true + supported_ISO15118_20_DC: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + hack_allow_bpt_with_iso2: true + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso_mux + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: DCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: SlacSimulator + imd: + config_implementation: + main: + selftest_success: true + module: IMDSimulator + ev_manager: + module: EvManager + 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 + dc_target_current: 20 + dc_target_voltage: 400 + 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 + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + 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: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse + error_history: + - module_id: error_history + implementation_id: error_history + error_history: + module: ErrorHistory + config_implementation: + error_history: + database_path: /tmp/error_history.db +x-module-layout: {} diff --git a/config/config-sil-dc-tls.yaml b/config/config-sil-dc-tls.yaml index 7645821b2..d1f32668d 100644 --- a/config/config-sil-dc-tls.yaml +++ b/config/config-sil-dc-tls.yaml @@ -4,6 +4,7 @@ active_modules: config_module: device: auto tls_security: force + tls_key_logging: true connections: security: - module_id: evse_security diff --git a/config/config-sil-energy-management.yaml b/config/config-sil-energy-management.yaml index 199f8b5e8..f8bc6de4c 100644 --- a/config/config-sil-energy-management.yaml +++ b/config/config-sil-energy-management.yaml @@ -15,6 +15,9 @@ active_modules: supported_ISO15118_2: true evse_manager_1: module: EvseManager + mapping: + module: + evse: 1 config_module: connector_id: 1 evse_id: DE*PNX*E12345*1 @@ -43,6 +46,9 @@ active_modules: implementation_id: charger evse_manager_2: module: EvseManager + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: DE*PNX*E12345*2 @@ -159,25 +165,56 @@ active_modules: connections: price_information: [] energy_consumer: - - module_id: evse_manager_1 + - module_id: evse_manager_1_sink implementation_id: energy_grid - - module_id: evse_manager_2 + - module_id: evse_manager_2_sink implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_1_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + evse_manager_2_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse random_delay: - module_id: evse_manager_1 implementation_id: random_delay error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_sink + implementation_id: external_limits + - module_id: evse_manager_2_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/config/config-sil-ocpp-custom-extension.yaml b/config/config-sil-ocpp-custom-extension.yaml index 0a13a162e..bd18e3ab8 100644 --- a/config/config-sil-ocpp-custom-extension.yaml +++ b/config/config-sil-ocpp-custom-extension.yaml @@ -15,7 +15,9 @@ active_modules: supported_ISO15118_2: true evse_manager_1: module: EvseManager - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 evse_id: "1" @@ -25,6 +27,7 @@ active_modules: 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 @@ -40,7 +43,9 @@ active_modules: implementation_id: charger evse_manager_2: module: EvseManager - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: "2" @@ -50,6 +55,7 @@ active_modules: 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 @@ -65,12 +71,16 @@ active_modules: implementation_id: charger yeti_driver_1: module: JsYetiSimulator - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 yeti_driver_2: module: JsYetiSimulator - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 slac: @@ -152,6 +162,13 @@ active_modules: data_transfer: - module_id: ocpp_extension implementation_id: data_transfer + evse_energy_sink: + - module_id: grid_connection_point + implementation_id: external_limits + - module_id: evse_manager_1_ocpp_sink + implementation_id: external_limits + - module_id: evse_manager_2_ocpp_sink + implementation_id: external_limits evse_security: module: EvseSecurity config_module: @@ -175,30 +192,94 @@ active_modules: energy_trunk: - module_id: grid_connection_point implementation_id: energy_grid - grid_connection_point: + evse_manager_1_ocpp_sink: module: EnergyNode + mapping: + module: + evse: 1 config_module: - fuse_limit_A: 40.0 + fuse_limit_A: 32.0 phase_count: 3 connections: - price_information: [] energy_consumer: - module_id: evse_manager_1 implementation_id: energy_grid + evse_manager_2_ocpp_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: - module_id: evse_manager_2 implementation_id: energy_grid + evse_manager_1_api_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1_ocpp_sink + implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_2_api_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2_ocpp_sink + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_2 + implementation_id: powermeter + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager_1_api_sink + implementation_id: energy_grid + - module_id: evse_manager_2_api_sink + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: ocpp_generic error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_api_sink + implementation_id: external_limits + - module_id: evse_manager_2_api_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/config/config-sil-ocpp-pnc.yaml b/config/config-sil-ocpp-pnc.yaml index 9771903a6..e5b092279 100644 --- a/config/config-sil-ocpp-pnc.yaml +++ b/config/config-sil-ocpp-pnc.yaml @@ -18,7 +18,9 @@ active_modules: is_cert_install_needed: true evse_manager_1: module: EvseManager - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 evse_id: "DE*PNX*00001" @@ -28,6 +30,8 @@ active_modules: ac_hlc_enabled: true ac_hlc_use_5percent: false ac_enforce_hlc: false + external_ready_to_start_charging: true + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_1 @@ -43,7 +47,9 @@ active_modules: implementation_id: charger evse_manager_2: module: EvseManager - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: "2" @@ -53,6 +59,8 @@ active_modules: ac_hlc_enabled: false ac_hlc_use_5percent: false ac_enforce_hlc: false + external_ready_to_start_charging: true + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_2 @@ -68,12 +76,16 @@ active_modules: implementation_id: charger yeti_driver_1: module: JsYetiSimulator - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 yeti_driver_2: module: JsYetiSimulator - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 slac: @@ -157,6 +169,13 @@ active_modules: security: - module_id: evse_security implementation_id: main + evse_energy_sink: + - module_id: grid_connection_point + implementation_id: external_limits + - module_id: evse_manager_1_ocpp_sink + implementation_id: external_limits + - module_id: evse_manager_2_ocpp_sink + implementation_id: external_limits evse_security: module: EvseSecurity config_module: @@ -169,30 +188,94 @@ active_modules: energy_trunk: - module_id: grid_connection_point implementation_id: energy_grid - grid_connection_point: + evse_manager_1_ocpp_sink: module: EnergyNode + mapping: + module: + evse: 1 config_module: - fuse_limit_A: 40.0 + fuse_limit_A: 32.0 phase_count: 3 connections: - price_information: [] energy_consumer: - module_id: evse_manager_1 implementation_id: energy_grid + evse_manager_2_ocpp_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: - module_id: evse_manager_2 implementation_id: energy_grid + evse_manager_1_api_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1_ocpp_sink + implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_2_api_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2_ocpp_sink + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_2 + implementation_id: powermeter + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager_1_api_sink + implementation_id: energy_grid + - module_id: evse_manager_2_api_sink + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: ocpp_generic error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_api_sink + implementation_id: external_limits + - module_id: evse_manager_2_api_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index 7ad065089..779d59300 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -16,11 +16,14 @@ active_modules: persistent_store: module: PersistentStore evse_manager_1: - evse: 1 + mapping: + module: + evse: 1 module: EvseManager config_module: connector_id: 1 evse_id: "1" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp/everest-logs @@ -47,10 +50,13 @@ active_modules: implementation_id: main evse_manager_2: module: EvseManager - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: "2" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp @@ -73,13 +79,17 @@ active_modules: - module_id: iso15118_charger implementation_id: charger yeti_driver_1: - evse: 1 module: JsYetiSimulator + mapping: + module: + evse: 1 config_module: connector_id: 1 yeti_driver_2: - evse: 2 module: JsYetiSimulator + mapping: + module: + evse: 2 config_module: connector_id: 2 slac: @@ -161,9 +171,13 @@ active_modules: display_message: - module_id: display_message implementation_id: display_message - connector_zero_sink: + evse_energy_sink: - module_id: grid_connection_point implementation_id: external_limits + - module_id: evse_manager_1_ocpp_sink + implementation_id: external_limits + - module_id: evse_manager_2_ocpp_sink + implementation_id: external_limits display_message: module: TerminalCostAndPriceMessage connections: @@ -182,33 +196,94 @@ active_modules: energy_trunk: - module_id: grid_connection_point implementation_id: energy_grid - grid_connection_point: + evse_manager_1_ocpp_sink: module: EnergyNode + mapping: + module: + evse: 1 config_module: - fuse_limit_A: 40.0 + fuse_limit_A: 32.0 phase_count: 3 connections: - price_information: [] energy_consumer: - module_id: evse_manager_1 implementation_id: energy_grid + evse_manager_2_ocpp_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: - module_id: evse_manager_2 implementation_id: energy_grid + evse_manager_1_api_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1_ocpp_sink + implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_2_api_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2_ocpp_sink + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_2 + implementation_id: powermeter + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager_1_api_sink + implementation_id: energy_grid + - module_id: evse_manager_2_api_sink + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse ocpp: - module_id: ocpp implementation_id: ocpp_generic error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_api_sink + implementation_id: external_limits + - module_id: evse_manager_2_api_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/config/config-sil-ocpp201-pnc.yaml b/config/config-sil-ocpp201-pnc.yaml index 834e353bf..7556d44b1 100644 --- a/config/config-sil-ocpp201-pnc.yaml +++ b/config/config-sil-ocpp201-pnc.yaml @@ -18,7 +18,9 @@ active_modules: is_cert_install_needed: true evse_manager_1: module: EvseManager - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 evse_id: "DE*PNX*00001" @@ -43,7 +45,9 @@ active_modules: implementation_id: charger evse_manager_2: module: EvseManager - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: "2" @@ -68,12 +72,16 @@ active_modules: implementation_id: charger yeti_driver_1: module: JsYetiSimulator - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 yeti_driver_2: module: JsYetiSimulator - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 slac: @@ -128,6 +136,16 @@ active_modules: security: - module_id: evse_security implementation_id: main + evse_energy_sink: + - module_id: grid_connection_point + implementation_id: external_limits + - module_id: evse_manager_1_ocpp_sink + 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: @@ -163,30 +181,94 @@ active_modules: energy_trunk: - module_id: grid_connection_point implementation_id: energy_grid - grid_connection_point: + evse_manager_1_ocpp_sink: module: EnergyNode + mapping: + module: + evse: 1 config_module: - fuse_limit_A: 40.0 + fuse_limit_A: 32.0 phase_count: 3 connections: - price_information: [] energy_consumer: - module_id: evse_manager_1 implementation_id: energy_grid + evse_manager_2_ocpp_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: - module_id: evse_manager_2 implementation_id: energy_grid + evse_manager_1_api_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1_ocpp_sink + implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_2_api_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2_ocpp_sink + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_2 + implementation_id: powermeter + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager_1_api_sink + implementation_id: energy_grid + - module_id: evse_manager_2_api_sink + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: ocpp_generic error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_api_sink + implementation_id: external_limits + - module_id: evse_manager_2_api_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/config/config-sil-ocpp201.yaml b/config/config-sil-ocpp201.yaml index 93ba79bb1..fe537cf15 100644 --- a/config/config-sil-ocpp201.yaml +++ b/config/config-sil-ocpp201.yaml @@ -15,10 +15,13 @@ active_modules: supported_ISO15118_2: true evse_manager_1: module: EvseManager - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 evse_id: "1" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp @@ -41,10 +44,13 @@ active_modules: implementation_id: charger evse_manager_2: module: EvseManager - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 evse_id: "2" + connector_type: "cType2" session_logging: true session_logging_xml: false session_logging_path: /tmp @@ -67,12 +73,16 @@ active_modules: implementation_id: charger yeti_driver_1: module: JsYetiSimulator - evse: 1 + mapping: + module: + evse: 1 config_module: connector_id: 1 yeti_driver_2: module: JsYetiSimulator - evse: 2 + mapping: + module: + evse: 2 config_module: connector_id: 2 slac: @@ -127,9 +137,16 @@ active_modules: security: - module_id: evse_security implementation_id: main - connector_zero_sink: + evse_energy_sink: - module_id: grid_connection_point implementation_id: external_limits + - module_id: evse_manager_1_ocpp_sink + 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: @@ -157,36 +174,103 @@ 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: energy_trunk: - module_id: grid_connection_point implementation_id: energy_grid - grid_connection_point: + evse_manager_1_ocpp_sink: module: EnergyNode + mapping: + module: + evse: 1 config_module: - fuse_limit_A: 40.0 + fuse_limit_A: 32.0 phase_count: 3 connections: - price_information: [] energy_consumer: - module_id: evse_manager_1 implementation_id: energy_grid + evse_manager_2_ocpp_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: - module_id: evse_manager_2 implementation_id: energy_grid + evse_manager_1_api_sink: + module: EnergyNode + mapping: + module: + evse: 1 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_1_ocpp_sink + implementation_id: energy_grid powermeter: - module_id: yeti_driver_1 implementation_id: powermeter + evse_manager_2_api_sink: + module: EnergyNode + mapping: + module: + evse: 2 + config_module: + fuse_limit_A: 32.0 + phase_count: 3 + connections: + energy_consumer: + - module_id: evse_manager_2_ocpp_sink + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver_2 + implementation_id: powermeter + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager_1_api_sink + implementation_id: energy_grid + - module_id: evse_manager_2_api_sink + implementation_id: energy_grid api: module: API connections: evse_manager: - module_id: evse_manager_1 implementation_id: evse + - module_id: evse_manager_2 + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: ocpp_generic error_history: - module_id: error_history implementation_id: error_history + evse_energy_sink: + - module_id: evse_manager_1_api_sink + implementation_id: external_limits + - module_id: evse_manager_2_api_sink + implementation_id: external_limits error_history: module: ErrorHistory config_implementation: diff --git a/dependencies.yaml b/dependencies.yaml index 7ea8c13da..02c6a2194 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -4,7 +4,7 @@ --- everest-framework: git: https://github.com/EVerest/everest-framework.git - git_tag: v0.17.1 + git_tag: v0.19.1 options: [ "BUILD_TESTING OFF", "everest-framework_USE_PYTHON_VENV ${PROJECT_NAME}_USE_PYTHON_VENV", @@ -17,10 +17,6 @@ sigslot: options: - "SIGSLOT_COMPILE_EXAMPLES OFF" - "SIGSLOT_COMPILE_TESTS OFF" -libmodbus: - git: https://github.com/EVerest/libmodbus.git - git_tag: v0.3.1 - cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBMODBUS" pugixml: git: https://github.com/zeux/pugixml git_tag: v1.12.1 @@ -43,6 +39,17 @@ libfsm: git_tag: v0.2.0 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBFSM" +# libcbv2g +libcbv2g: + git: https://github.com/EVerest/libcbv2g.git + git_tag: v0.2.1 + cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBCBV2G" +# libiso15118 +libiso15118: + git: https://github.com/EVerest/libiso15118.git + git_tag: v0.3.0 + cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBISO15118" + # LEM DCBM 400/600 module libcurl: git: https://github.com/curl/curl.git @@ -50,28 +57,23 @@ libcurl: cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBCURL" # EvseSecurity -# This has to appear before libocpp in this file since it is also a direct dependency of libocpp -# and would otherwise be overwritten by the version used there +# This has to appear before libocpp in this file since it is also a direct dependency +# of libocpp and would otherwise be overwritten by the version used there libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.8.0 + git_tag: v0.9.2 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBEVSE_SECURITY" # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: v0.17.0 + git_tag: c66383782a32827920af80314165843deed63c98 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBOCPP" # Josev Josev: git: https://github.com/EVerest/ext-switchev-iso15118.git - git_tag: 2024.9.0 + git_tag: 2024.10.0 cmake_condition: "EVEREST_ENABLE_PY_SUPPORT AND EVEREST_DEPENDENCY_ENABLED_JOSEV" -# libcbv2g -libcbv2g: - git: https://github.com/EVerest/libcbv2g.git - git_tag: v0.2.0 - cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBCBV2G" # mbedtls ext-mbedtls: git: https://github.com/EVerest/ext-mbedtls.git @@ -84,7 +86,7 @@ ext-mbedtls: # everest-testing and ev-dev-tools everest-utils: git: https://github.com/EVerest/everest-utils.git - git_tag: v0.3.1 + git_tag: v0.4.4 # unit testing gtest: diff --git a/interfaces/ISO15118_charger.yaml b/interfaces/ISO15118_charger.yaml index f8a17d875..a322aa6e2 100644 --- a/interfaces/ISO15118_charger.yaml +++ b/interfaces/ISO15118_charger.yaml @@ -14,9 +14,9 @@ cmds: description: Available energy transfer modes supported by the EVSE type: array items: - description: The different energy modes supported by the SECC - type: string - $ref: /iso15118_charger#/EnergyTransferMode + description: The different energy modes supported by the SECC + type: object + $ref: /iso15118_charger#/SupportedEnergyMode minItems: 1 maxItems: 6 sae_j2847_mode: @@ -346,3 +346,19 @@ vars: description: >- Debug - Contains the selected protocol type: string + display_parameters: + description: >- + Parameters that may be displayed on the EVSE (Soc, battery capacity) + type: object + $ref: /iso15118_charger#/DisplayParameters + d20_dc_dynamic_charge_mode: + description: >- + The parameters the EVCC offers and sets for dynamic control mode + type: object + $ref: /iso15118_charger#/DcChargeDynamicModeValues + dc_ev_present_voltage: + description: Present Voltage measured from the EV + type: number + meter_info_requested: + description: The EV requested meter infos from the EVSE + type: "null" diff --git a/interfaces/evse_manager.yaml b/interfaces/evse_manager.yaml index 39c14a01e..3478d3997 100644 --- a/interfaces/evse_manager.yaml +++ b/interfaces/evse_manager.yaml @@ -26,7 +26,7 @@ cmds: type: boolean authorize_response: description: >- - Reports the result of an authorization request to the EvseManager. + Reports the result of an authorization request to the EvseManager. Contains the provided_token for which authorization was requested and the validation_result arguments: @@ -49,8 +49,9 @@ cmds: arguments: reservation_id: description: >- - The reservation id (should be added to the TransactionStarted - event) + The reservation id (should be added to the TransactionStarted event). Set this to a negative value if there is + no specific reservation id for this evse but the evse should still move to a Reserved state because of total + global reservations. type: integer result: description: Returns true if the EVSE accepted the reservation, else false. @@ -101,13 +102,6 @@ cmds: result: description: Returns true if unlocking sequence was successfully executed type: boolean - set_external_limits: - description: Set additional external energy flow limits at this node. - arguments: - value: - description: UUID of node that this limit applies to - type: object - $ref: /energy#/ExternalLimits set_get_certificate_response: description: >- CertificateInstallationRes/CertificateUpdateRes - Set the new/updated Contract Certificate (including the certificate chain) @@ -150,6 +144,9 @@ vars: description: Measured dataset type: object $ref: /powermeter#/Powermeter + powermeter_public_key_ocmf: + description: Powermeter public key + type: string evse_id: description: EVSE ID including the connector number, e.g. DE*PNX*E123456*1 type: string @@ -159,7 +156,7 @@ vars: $ref: /evse_board_support#/HardwareCapabilities iso15118_certificate_request: description: >- - The vehicle requests the SECC to deliver the certificate that belong + The vehicle requests the SECC to deliver the certificate that belong to the currently valid contract of the vehicle. Response will be reported async via set_get_certificate_response type: object @@ -181,4 +178,4 @@ vars: Contains the selected protocol used for charging for informative purposes type: string errors: - - reference: /errors/evse_manager \ No newline at end of file + - reference: /errors/evse_manager diff --git a/interfaces/evse_security.yaml b/interfaces/evse_security.yaml index 1bc784415..44d4bcfc6 100644 --- a/interfaces/evse_security.yaml +++ b/interfaces/evse_security.yaml @@ -154,6 +154,26 @@ cmds: description: The response to the requested command type: object $ref: /evse_security#/GetCertificateInfoResult + get_all_valid_certificates_info: + description: >- + Finds the latest valid leafs, for each root certificate that is present on the filesystem, + and returns all the newest valid leafs that are present for different roots + arguments: + certificate_type: + description: Specifies the leaf certificate type + type: string + $ref: /evse_security#/LeafCertificateType + encoding: + description: Specifies the encoding of the key + type: string + $ref: /evse_security#/EncodingFormat + include_ocsp: + description: Specifies whether per-certificate OCSP data is also requested + type: boolean + result: + description: The response to the requested command + type: object + $ref: /evse_security#/GetCertificateFullInfoResult get_verify_file: description: Command to get the file path of a CA bundle that can be used for verification arguments: @@ -164,6 +184,16 @@ cmds: result: description: The path of the CA bundle file type: string + get_verify_location: + description: Command to get the file path of the CA root directory that can be used for verification. Will also invoke c_rehash for that directory + arguments: + certificate_type: + description: Specifies that CA certificate type + type: string + $ref: /evse_security#/CaCertificateType + result: + description: The path of the CA certificates directory + type: string get_leaf_expiry_days_count: description: >- Command to get the days count until the given leaf certificate expires. diff --git a/interfaces/power_supply_DC.yaml b/interfaces/power_supply_DC.yaml index 6f2ba9717..cecb3a327 100644 --- a/interfaces/power_supply_DC.yaml +++ b/interfaces/power_supply_DC.yaml @@ -12,7 +12,15 @@ description: >- (e.g. communication to the hardware is lost), the driver module shall cache the last mode and voltage/current settings. Once the PSU is back on-line (e.g. after a CommunicationFault), set the last mode and voltage/current value received and only after that clear the error. - 3) var voltage_current shall be published on regular intervals. The interval depends on the hardware, but it shall be at least once per second. If possible, + 3) setMode to Off requires special attention. To avoid switching the output relays of the charger off under full load, make sure to return + from the setMode function(Off) only when the current is below a safe threshold for switching off the relays (exact value is hardware dependent). + If communication is lost with the power supply, make sure to still return, the call must not block for a longer period of time. + EVerest will ensure the order of the calls is correct during shutdown, but will not wait for the power supply to actually turn off: + 1. call setMode(Off) on power_supply_DC + 2. call allow_power_on(false) on evse_board_support + If the setMode(Off) returns immediately, it may happen that the bsp implementation opens the relays before the power supply is shutdown. + + 4) var voltage_current shall be published on regular intervals. The interval depends on the hardware, but it shall be at least once per second. If possible, update at e.g. 4 Hertz is recommended. cmds: setMode: 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/CMakeLists.txt b/lib/staging/CMakeLists.txt index 75f6d0c89..b0bed3f7e 100644 --- a/lib/staging/CMakeLists.txt +++ b/lib/staging/CMakeLists.txt @@ -1,14 +1,21 @@ add_subdirectory(can_dpm1000) +add_subdirectory(external_energy_limits) +add_subdirectory(helpers) +add_subdirectory(util) + if(EVEREST_DEPENDENCY_ENABLED_LIBEVSE_SECURITY) add_subdirectory(evse_security) add_subdirectory(tls) endif() + if(EVEREST_DEPENDENCY_ENABLED_LIBSLAC AND EVEREST_DEPENDENCY_ENABLED_LIBFSM) add_subdirectory(slac) endif() + if(EVEREST_DEPENDENCY_ENABLED_EVEREST_GPIO) add_subdirectory(gpio) endif() + if(EVEREST_DEPENDENCY_ENABLED_LIBOCPP) add_subdirectory(ocpp) endif() diff --git a/lib/staging/evse_security/conversions.cpp b/lib/staging/evse_security/conversions.cpp index 216c48807..909f98962 100644 --- a/lib/staging/evse_security/conversions.cpp +++ b/lib/staging/evse_security/conversions.cpp @@ -451,6 +451,7 @@ types::evse_security::OCSPRequestDataList to_everest(evse_security::OCSPRequestD types::evse_security::CertificateInfo to_everest(evse_security::CertificateInfo other) { types::evse_security::CertificateInfo lhs; lhs.key = other.key; + lhs.certificate_root = other.certificate_root; lhs.certificate = other.certificate; lhs.certificate_single = other.certificate_single; lhs.password = other.password; diff --git a/lib/staging/external_energy_limits/CMakeLists.txt b/lib/staging/external_energy_limits/CMakeLists.txt new file mode 100644 index 000000000..cdf7f3fac --- /dev/null +++ b/lib/staging/external_energy_limits/CMakeLists.txt @@ -0,0 +1,22 @@ +# External Energy Limits + +add_library(external_energy_limits STATIC) +add_library(everest::external_energy_limits ALIAS external_energy_limits) + +target_sources(external_energy_limits + PRIVATE + external_energy_limits.cpp +) + +target_include_directories(external_energy_limits + PUBLIC + $ + "$" +) + +add_dependencies(external_energy_limits generate_cpp_files) + +target_link_libraries(external_energy_limits + PRIVATE + everest::framework +) \ No newline at end of file diff --git a/lib/staging/external_energy_limits/external_energy_limits.cpp b/lib/staging/external_energy_limits/external_energy_limits.cpp new file mode 100644 index 000000000..b16a0f756 --- /dev/null +++ b/lib/staging/external_energy_limits/external_energy_limits.cpp @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#include + +namespace external_energy_limits { + +bool is_evse_sink_configured(const std::vector>& r_evse_energy_sink, + const int32_t evse_id) { + for (const auto& evse_sink : r_evse_energy_sink) { + if (not evse_sink->get_mapping().has_value()) { + EVLOG_critical << "Please configure an evse mapping your configuration file for the connected " + "r_evse_energy_sink with module_id: " + << evse_sink->module_id; + throw std::runtime_error("No mapping configured for evse_id: " + evse_id); + } + if (evse_sink->get_mapping().value().evse == evse_id) { + return true; + } + } + return false; +} + +external_energy_limitsIntf& +get_evse_sink_by_evse_id(const std::vector>& r_evse_energy_sink, + const int32_t evse_id) { + for (const auto& evse_sink : r_evse_energy_sink) { + if (not evse_sink->get_mapping().has_value()) { + EVLOG_critical << "Please configure an evse mapping your configuration file for the connected " + "r_evse_energy_sink with module_id: " + << evse_sink->module_id; + throw std::runtime_error("No mapping configured for evse_id: " + evse_id); + } + if (evse_sink->get_mapping().value().evse == evse_id) { + return *evse_sink; + } + } + throw std::runtime_error("No mapping configured for evse"); +} + +} // namespace external_energy_limits \ No newline at end of file diff --git a/lib/staging/external_energy_limits/external_energy_limits.hpp b/lib/staging/external_energy_limits/external_energy_limits.hpp new file mode 100644 index 000000000..73792d81a --- /dev/null +++ b/lib/staging/external_energy_limits/external_energy_limits.hpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +namespace external_energy_limits { + +/// \brief Checks if \p r_evse_energy_sink vector contains an element that has a mapping to the given \p evse_id +/// \param r_evse_energy_sink +/// \param evse_id +/// \return +bool is_evse_sink_configured(const std::vector>& r_evse_energy_sink, + const int32_t evse_id); + +/// \brief Returns the reference of external_energy_limitsIntf in \p r_evse_energy_sink that maps to the given \p +/// evse_id \param r_evse_energy_sink \param evse_id \return +external_energy_limitsIntf& +get_evse_sink_by_evse_id(const std::vector>& r_evse_energy_sink, + const int32_t evse_id); + +} // namespace external_energy_limits diff --git a/lib/staging/helpers/BUILD.bazel b/lib/staging/helpers/BUILD.bazel new file mode 100644 index 000000000..bfe0f7339 --- /dev/null +++ b/lib/staging/helpers/BUILD.bazel @@ -0,0 +1,13 @@ +cc_library( + name = "helpers", + srcs = ["lib/helpers.cpp"], + hdrs = ["include/everest/staging/helpers/helpers.hpp"], + copts = ["-std=c++17"], + visibility = ["//visibility:public"], + includes = ["include"], + deps = [ + "@com_github_fmtlib_fmt//:fmt", + "@com_github_nlohmann_json//:json", + "//types:types_lib", + ], +) diff --git a/lib/staging/helpers/CMakeLists.txt b/lib/staging/helpers/CMakeLists.txt new file mode 100644 index 000000000..008972fdb --- /dev/null +++ b/lib/staging/helpers/CMakeLists.txt @@ -0,0 +1,28 @@ +# EVerest helper functions + +add_library(everest_staging_helpers STATIC) +add_library(everest::staging::helpers ALIAS everest_staging_helpers) + +target_sources(everest_staging_helpers + PRIVATE + lib/helpers.cpp +) + +target_include_directories(everest_staging_helpers + PUBLIC + $ + "$" + $ +) + +target_link_libraries(everest_staging_helpers + PRIVATE + fmt::fmt + nlohmann_json::nlohmann_json +) + +add_dependencies(everest_staging_helpers generate_cpp_files) + +if (BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/lib/staging/helpers/include/everest/staging/helpers/helpers.hpp b/lib/staging/helpers/include/everest/staging/helpers/helpers.hpp new file mode 100644 index 000000000..831637c5f --- /dev/null +++ b/lib/staging/helpers/include/everest/staging/helpers/helpers.hpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#ifndef EVEREST_STAGING_HELPERS_HPP +#define EVEREST_STAGING_HELPERS_HPP + +#include + +namespace types::authorization { +struct ProvidedIdToken; +} + +namespace everest::staging::helpers { + +/// \brief Redacts a provided \p token by hashing it +/// \returns a hashed version of the provided token +std::string redact(const std::string& token); + +types::authorization::ProvidedIdToken redact(const types::authorization::ProvidedIdToken& token); + +} // namespace everest::staging::helpers + +#endif diff --git a/lib/staging/helpers/lib/helpers.cpp b/lib/staging/helpers/lib/helpers.cpp new file mode 100644 index 000000000..cdd86439b --- /dev/null +++ b/lib/staging/helpers/lib/helpers.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include + +#include + +#include + +#include + +namespace everest::staging::helpers { +std::string redact(const std::string& token) { + auto hash = std::hash{}(token); + return fmt::format("[redacted] hash: {:X}", hash); +} + +types::authorization::ProvidedIdToken redact(const types::authorization::ProvidedIdToken& token) { + types::authorization::ProvidedIdToken redacted_token = token; + redacted_token.id_token.value = redact(redacted_token.id_token.value); + if (redacted_token.parent_id_token.has_value()) { + auto& parent_id_token = redacted_token.parent_id_token.value(); + parent_id_token.value = redact(parent_id_token.value); + } + return redacted_token; +} +} // namespace everest::staging::helpers diff --git a/lib/staging/helpers/tests/CMakeLists.txt b/lib/staging/helpers/tests/CMakeLists.txt new file mode 100644 index 000000000..217f72525 --- /dev/null +++ b/lib/staging/helpers/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +set(TEST_TARGET_NAME ${PROJECT_NAME}_helpers_tests) + +add_executable(${TEST_TARGET_NAME} +helpers_test.cpp +) + +target_link_libraries(${TEST_TARGET_NAME} + PRIVATE + GTest::gmock_main + GTest::gtest_main + everest::staging::helpers +) + +include(GoogleTest) +gtest_discover_tests(${TEST_TARGET_NAME}) diff --git a/lib/staging/helpers/tests/helpers_test.cpp b/lib/staging/helpers/tests/helpers_test.cpp new file mode 100644 index 000000000..e92baabd5 --- /dev/null +++ b/lib/staging/helpers/tests/helpers_test.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include +#include + +#include + +#include + +using namespace everest::staging::helpers; +using ::testing::StartsWith; + +TEST(HelpersTest, redact_token) { + std::string token = "secret token"; + + auto redacted = redact(token); + + EXPECT_THAT(redacted, StartsWith("[redacted] hash: ")); +} diff --git a/lib/staging/ocpp/CMakeLists.txt b/lib/staging/ocpp/CMakeLists.txt index 67e104719..591e0f234 100644 --- a/lib/staging/ocpp/CMakeLists.txt +++ b/lib/staging/ocpp/CMakeLists.txt @@ -14,6 +14,12 @@ target_include_directories(ocpp_evse_security "$" ) +target_compile_options(ocpp_evse_security + PRIVATE + -Wimplicit-fallthrough + -Werror=switch-enum +) + add_dependencies(ocpp_evse_security generate_cpp_files) target_link_libraries(ocpp_evse_security @@ -39,6 +45,12 @@ target_include_directories(ocpp_conversions "$" ) +target_compile_options(ocpp_conversions + PRIVATE + -Wimplicit-fallthrough + -Werror=switch-enum +) + add_dependencies(ocpp_conversions generate_cpp_files) target_link_libraries(ocpp_conversions @@ -46,3 +58,4 @@ target_link_libraries(ocpp_conversions everest::ocpp everest::framework ) + diff --git a/lib/staging/ocpp/evse_security_ocpp.cpp b/lib/staging/ocpp/evse_security_ocpp.cpp index e79a7b46c..74e8146ba 100644 --- a/lib/staging/ocpp/evse_security_ocpp.cpp +++ b/lib/staging/ocpp/evse_security_ocpp.cpp @@ -124,6 +124,10 @@ std::string EvseSecurity::get_verify_file(const ocpp::CaCertificateType& certifi return this->r_security.call_get_verify_file(conversions::from_ocpp(certificate_type)); } +std::string EvseSecurity::get_verify_location(const ocpp::CaCertificateType& certificate_type) { + return this->r_security.call_get_verify_location(conversions::from_ocpp(certificate_type)); +} + int EvseSecurity::get_leaf_expiry_days_count(const ocpp::CertificateSigningUseEnum& certificate_type) { return this->r_security.call_get_leaf_expiry_days_count(conversions::from_ocpp(certificate_type)); } @@ -140,10 +144,8 @@ ocpp::CaCertificateType to_ocpp(types::evse_security::CaCertificateType other) { return ocpp::CaCertificateType::CSMS; case types::evse_security::CaCertificateType::MF: return ocpp::CaCertificateType::MF; - default: - throw std::runtime_error( - "Could not convert types::evse_security::CaCertificateType to ocpp::CaCertificateType"); } + throw std::runtime_error("Could not convert types::evse_security::CaCertificateType to ocpp::CaCertificateType"); } ocpp::LeafCertificateType to_ocpp(types::evse_security::LeafCertificateType other) { @@ -156,10 +158,9 @@ ocpp::LeafCertificateType to_ocpp(types::evse_security::LeafCertificateType othe return ocpp::LeafCertificateType::MF; case types::evse_security::LeafCertificateType::MO: return ocpp::LeafCertificateType::MO; - default: - throw std::runtime_error( - "Could not convert types::evse_security::LeafCertificateType to ocpp::CertificateSigningUseEnum"); } + throw std::runtime_error( + "Could not convert types::evse_security::LeafCertificateType to ocpp::CertificateSigningUseEnum"); } ocpp::CertificateType to_ocpp(types::evse_security::CertificateType other) { @@ -174,9 +175,8 @@ ocpp::CertificateType to_ocpp(types::evse_security::CertificateType other) { return ocpp::CertificateType::V2GCertificateChain; case types::evse_security::CertificateType::MFRootCertificate: return ocpp::CertificateType::MFRootCertificate; - default: - throw std::runtime_error("Could not convert types::evse_security::CertificateType to ocpp::CertificateType"); } + throw std::runtime_error("Could not convert types::evse_security::CertificateType to ocpp::CertificateType"); } ocpp::HashAlgorithmEnumType to_ocpp(types::evse_security::HashAlgorithm other) { @@ -187,10 +187,8 @@ ocpp::HashAlgorithmEnumType to_ocpp(types::evse_security::HashAlgorithm other) { return ocpp::HashAlgorithmEnumType::SHA384; case types::evse_security::HashAlgorithm::SHA512: return ocpp::HashAlgorithmEnumType::SHA512; - default: - throw std::runtime_error( - "Could not convert types::evse_security::HashAlgorithm to ocpp::HashAlgorithmEnumType"); } + throw std::runtime_error("Could not convert types::evse_security::HashAlgorithm to ocpp::HashAlgorithmEnumType"); } ocpp::InstallCertificateResult to_ocpp(types::evse_security::InstallCertificateResult other) { @@ -213,10 +211,9 @@ ocpp::InstallCertificateResult to_ocpp(types::evse_security::InstallCertificateR return ocpp::InstallCertificateResult::WriteError; case types::evse_security::InstallCertificateResult::Accepted: return ocpp::InstallCertificateResult::Accepted; - default: - throw std::runtime_error( - "Could not convert types::evse_security::InstallCertificateResult to ocpp::InstallCertificateResult"); } + throw std::runtime_error( + "Could not convert types::evse_security::InstallCertificateResult to ocpp::InstallCertificateResult"); } ocpp::CertificateValidationResult to_ocpp(types::evse_security::CertificateValidationResult other) { @@ -233,10 +230,11 @@ ocpp::CertificateValidationResult to_ocpp(types::evse_security::CertificateValid return ocpp::CertificateValidationResult::InvalidChain; case types::evse_security::CertificateValidationResult::Unknown: return ocpp::CertificateValidationResult::Unknown; - default: - throw std::runtime_error("Could not convert types::evse_security::CertificateValidationResult to " - "ocpp::CertificateValidationResult"); + case types::evse_security::CertificateValidationResult::Expired: + return ocpp::CertificateValidationResult::Expired; } + throw std::runtime_error("Could not convert types::evse_security::CertificateValidationResult to " + "ocpp::CertificateValidationResult"); } ocpp::GetCertificateInfoStatus to_ocpp(types::evse_security::GetCertificateInfoStatus other) { @@ -251,10 +249,9 @@ ocpp::GetCertificateInfoStatus to_ocpp(types::evse_security::GetCertificateInfoS return ocpp::GetCertificateInfoStatus::NotFoundValid; case types::evse_security::GetCertificateInfoStatus::PrivateKeyNotFound: return ocpp::GetCertificateInfoStatus::PrivateKeyNotFound; - default: - throw std::runtime_error("Could not convert types::evse_security::GetCertificateInfoStatus to " - "ocpp::GetCertificateInfoStatus"); } + throw std::runtime_error("Could not convert types::evse_security::GetCertificateInfoStatus to " + "ocpp::GetCertificateInfoStatus"); } ocpp::DeleteCertificateResult to_ocpp(types::evse_security::DeleteCertificateResult other) { @@ -265,10 +262,9 @@ ocpp::DeleteCertificateResult to_ocpp(types::evse_security::DeleteCertificateRes return ocpp::DeleteCertificateResult::Failed; case types::evse_security::DeleteCertificateResult::NotFound: return ocpp::DeleteCertificateResult::NotFound; - default: - throw std::runtime_error( - "Could not convert types::evse_security::DeleteCertificateResult to ocpp::DeleteCertificateResult"); } + throw std::runtime_error( + "Could not convert types::evse_security::DeleteCertificateResult to ocpp::DeleteCertificateResult"); } ocpp::GetCertificateSignRequestStatus to_ocpp(types::evse_security::GetCertificateSignRequestStatus other) { @@ -281,10 +277,9 @@ ocpp::GetCertificateSignRequestStatus to_ocpp(types::evse_security::GetCertifica return ocpp::GetCertificateSignRequestStatus::KeyGenError; case types::evse_security::GetCertificateSignRequestStatus::GenerationError: return ocpp::GetCertificateSignRequestStatus::GenerationError; - default: - throw std::runtime_error("Could not convert types::evse_security::GetCertificateSignRequestStatus to " - "ocpp::GetCertificateSignRequestStatus"); } + throw std::runtime_error("Could not convert types::evse_security::GetCertificateSignRequestStatus to " + "ocpp::GetCertificateSignRequestStatus"); } ocpp::CertificateHashDataType to_ocpp(types::evse_security::CertificateHashData other) { @@ -362,10 +357,8 @@ types::evse_security::CaCertificateType from_ocpp(ocpp::CaCertificateType other) return types::evse_security::CaCertificateType::CSMS; case ocpp::CaCertificateType::MF: return types::evse_security::CaCertificateType::MF; - default: - throw std::runtime_error( - "Could not convert types::evse_security::CaCertificateType to ocpp::CaCertificateType"); } + throw std::runtime_error("Could not convert types::evse_security::CaCertificateType to ocpp::CaCertificateType"); } types::evse_security::LeafCertificateType from_ocpp(ocpp::CertificateSigningUseEnum other) { @@ -376,10 +369,9 @@ types::evse_security::LeafCertificateType from_ocpp(ocpp::CertificateSigningUseE return types::evse_security::LeafCertificateType::V2G; case ocpp::CertificateSigningUseEnum::ManufacturerCertificate: return types::evse_security::LeafCertificateType::MF; - default: - throw std::runtime_error( - "Could not convert ocpp::CertificateSigningUseEnum to types::evse_security::LeafCertificateType"); } + throw std::runtime_error( + "Could not convert ocpp::CertificateSigningUseEnum to types::evse_security::LeafCertificateType"); } types::evse_security::LeafCertificateType from_ocpp(ocpp::LeafCertificateType other) { @@ -392,10 +384,9 @@ types::evse_security::LeafCertificateType from_ocpp(ocpp::LeafCertificateType ot return types::evse_security::LeafCertificateType::MF; case ocpp::LeafCertificateType::MO: return types::evse_security::LeafCertificateType::MO; - default: - throw std::runtime_error( - "Could not convert ocpp::CertificateSigningUseEnum to types::evse_security::LeafCertificateType"); } + throw std::runtime_error( + "Could not convert ocpp::CertificateSigningUseEnum to types::evse_security::LeafCertificateType"); } types::evse_security::CertificateType from_ocpp(ocpp::CertificateType other) { @@ -410,9 +401,8 @@ types::evse_security::CertificateType from_ocpp(ocpp::CertificateType other) { return types::evse_security::CertificateType::V2GCertificateChain; case ocpp::CertificateType::MFRootCertificate: return types::evse_security::CertificateType::MFRootCertificate; - default: - throw std::runtime_error("Could not convert ocpp::CertificateType to types::evse_security::CertificateType"); } + throw std::runtime_error("Could not convert ocpp::CertificateType to types::evse_security::CertificateType"); } types::evse_security::HashAlgorithm from_ocpp(ocpp::HashAlgorithmEnumType other) { @@ -423,10 +413,8 @@ types::evse_security::HashAlgorithm from_ocpp(ocpp::HashAlgorithmEnumType other) return types::evse_security::HashAlgorithm::SHA384; case ocpp::HashAlgorithmEnumType::SHA512: return types::evse_security::HashAlgorithm::SHA512; - default: - throw std::runtime_error( - "Could not convert ocpp::HashAlgorithmEnumType to types::evse_security::HashAlgorithm"); } + throw std::runtime_error("Could not convert ocpp::HashAlgorithmEnumType to types::evse_security::HashAlgorithm"); } types::evse_security::InstallCertificateResult from_ocpp(ocpp::InstallCertificateResult other) { @@ -449,10 +437,9 @@ types::evse_security::InstallCertificateResult from_ocpp(ocpp::InstallCertificat return types::evse_security::InstallCertificateResult::WriteError; case ocpp::InstallCertificateResult::Accepted: return types::evse_security::InstallCertificateResult::Accepted; - default: - throw std::runtime_error( - "Could not convert ocpp::InstallCertificateResult to types::evse_security::InstallCertificateResult"); } + throw std::runtime_error( + "Could not convert ocpp::InstallCertificateResult to types::evse_security::InstallCertificateResult"); } types::evse_security::DeleteCertificateResult from_ocpp(ocpp::DeleteCertificateResult other) { @@ -463,10 +450,9 @@ types::evse_security::DeleteCertificateResult from_ocpp(ocpp::DeleteCertificateR return types::evse_security::DeleteCertificateResult::Failed; case ocpp::DeleteCertificateResult::NotFound: return types::evse_security::DeleteCertificateResult::NotFound; - default: - throw std::runtime_error( - "Could not convert ocpp::DeleteCertificateResult to types::evse_security::DeleteCertificateResult"); } + throw std::runtime_error( + "Could not convert ocpp::DeleteCertificateResult to types::evse_security::DeleteCertificateResult"); } types::evse_security::CertificateHashData from_ocpp(ocpp::CertificateHashDataType other) { diff --git a/lib/staging/ocpp/evse_security_ocpp.hpp b/lib/staging/ocpp/evse_security_ocpp.hpp index 8a7ff6857..21c057cd9 100644 --- a/lib/staging/ocpp/evse_security_ocpp.hpp +++ b/lib/staging/ocpp/evse_security_ocpp.hpp @@ -40,6 +40,7 @@ class EvseSecurity : public ocpp::EvseSecurity { bool include_ocsp) override; bool update_certificate_links(const ocpp::CertificateSigningUseEnum& certificate_type) override; std::string get_verify_file(const ocpp::CaCertificateType& certificate_type) override; + std::string get_verify_location(const ocpp::CaCertificateType& certificate_type) override; int get_leaf_expiry_days_count(const ocpp::CertificateSigningUseEnum& certificate_type) override; }; diff --git a/lib/staging/ocpp/ocpp_conversions.cpp b/lib/staging/ocpp/ocpp_conversions.cpp index 5211c2ff2..7c731171d 100644 --- a/lib/staging/ocpp/ocpp_conversions.cpp +++ b/lib/staging/ocpp/ocpp_conversions.cpp @@ -1,5 +1,7 @@ #include "ocpp_conversions.hpp" +#include + #include "generated/types/display_message.hpp" namespace ocpp_conversions { @@ -12,10 +14,9 @@ to_everest_display_message_priority(const ocpp::v201::MessagePriorityEnum& prior return types::display_message::MessagePriorityEnum::InFront; case ocpp::v201::MessagePriorityEnum::NormalCycle: return types::display_message::MessagePriorityEnum::NormalCycle; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::MessagePriorityEnum to types::display_message::MessagePriorityEnum"); } + throw std::out_of_range( + "Could not convert ocpp::v201::MessagePriorityEnum to types::display_message::MessagePriorityEnum"); } ocpp::v201::MessagePriorityEnum @@ -27,10 +28,9 @@ to_ocpp_201_message_priority(const types::display_message::MessagePriorityEnum& return ocpp::v201::MessagePriorityEnum::InFront; case types::display_message::MessagePriorityEnum::NormalCycle: return ocpp::v201::MessagePriorityEnum::NormalCycle; - default: - throw std::out_of_range( - "Could not convert types::display_message::MessagePriorityEnum to ocpp::v201::MessagePriorityEnum"); } + throw std::out_of_range( + "Could not convert types::display_message::MessagePriorityEnum to ocpp::v201::MessagePriorityEnum"); } types::display_message::MessageStateEnum to_everest_display_message_state(const ocpp::v201::MessageStateEnum& state) { @@ -43,10 +43,9 @@ types::display_message::MessageStateEnum to_everest_display_message_state(const return types::display_message::MessageStateEnum::Idle; case ocpp::v201::MessageStateEnum::Unavailable: return types::display_message::MessageStateEnum::Unavailable; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::MessageStateEnum to types::display_message::MessageStateEnum"); } + throw std::out_of_range( + "Could not convert ocpp::v201::MessageStateEnum to types::display_message::MessageStateEnum"); } ocpp::v201::MessageStateEnum to_ocpp_201_display_message_state(const types::display_message::MessageStateEnum& state) { @@ -59,10 +58,9 @@ ocpp::v201::MessageStateEnum to_ocpp_201_display_message_state(const types::disp return ocpp::v201::MessageStateEnum::Idle; case types::display_message::MessageStateEnum::Unavailable: return ocpp::v201::MessageStateEnum::Unavailable; - default: - throw std::out_of_range( - "Could not convert types::display_message::MessageStateEnum to ocpp::v201::MessageStateEnum"); } + throw std::out_of_range( + "Could not convert types::display_message::MessageStateEnum to ocpp::v201::MessageStateEnum"); } types::display_message::MessageFormat @@ -76,10 +74,8 @@ to_everest_display_message_format(const ocpp::v201::MessageFormatEnum& message_f return types::display_message::MessageFormat::URI; case ocpp::v201::MessageFormatEnum::UTF8: return types::display_message::MessageFormat::UTF8; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::MessageFormat to types::display_message::MessageFormatEnum"); } + throw std::out_of_range("Could not convert ocpp::v201::MessageFormat to types::display_message::MessageFormatEnum"); } ocpp::v201::MessageFormatEnum to_ocpp_201_message_format_enum(const types::display_message::MessageFormat& format) { @@ -191,8 +187,16 @@ ocpp::DisplayMessage to_ocpp_display_message(const types::display_message::Displ m.state = to_ocpp_201_display_message_state(display_message.state.value()); } - m.timestamp_from = display_message.timestamp_from; - m.timestamp_to = display_message.timestamp_to; + try { + if (display_message.timestamp_from.has_value()) { + m.timestamp_from = ocpp::DateTime(display_message.timestamp_from.value()); + } + if (display_message.timestamp_to.has_value()) { + m.timestamp_to = ocpp::DateTime(display_message.timestamp_to.value()); + } + } catch (const ocpp::TimePointParseException& e) { + EVLOG_warning << "Could not parse timestamp when converting DisplayMessage: " << e.what(); + } return m; } @@ -205,9 +209,8 @@ types::session_cost::SessionStatus to_everest_running_cost_state(const ocpp::Run return types::session_cost::SessionStatus::Idle; case ocpp::RunningCostState::Finished: return types::session_cost::SessionStatus::Finished; - default: - throw std::out_of_range("Could not convert ocpp::RunningCostState to types::session_cost::SessionStatus"); } + throw std::out_of_range("Could not convert ocpp::RunningCostState to types::session_cost::SessionStatus"); } types::session_cost::SessionCostChunk create_session_cost_chunk(const double& price, const uint32_t& number_of_decimals, @@ -354,4 +357,32 @@ types::session_cost::SessionCost create_session_cost(const ocpp::RunningCost& ru return cost; } + +ocpp::DateTime to_ocpp_datetime_or_now(const std::string& datetime_string) { + std::optional timestamp; + try { + return ocpp::DateTime(datetime_string); + } catch (const ocpp::TimePointParseException& e) { + EVLOG_warning << "Could not parse datetime string: " << e.what() << ". Using current DateTime instead"; + } + 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 19449ae86..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 @@ -44,4 +45,11 @@ create_charging_price_component(const double& price, const uint32_t& number_of_d types::session_cost::SessionCost create_session_cost(const ocpp::RunningCost& running_cost, const uint32_t number_of_decimals, std::optional currency_code); + +/// \brief Convert given \p datetime_string in RFC3339 format to a OCPP DateTime. If this is not possible return the +/// 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/lib/staging/tls/CMakeLists.txt b/lib/staging/tls/CMakeLists.txt index 50318a13c..17188f8c9 100644 --- a/lib/staging/tls/CMakeLists.txt +++ b/lib/staging/tls/CMakeLists.txt @@ -19,14 +19,16 @@ target_compile_definitions(tls PRIVATE target_include_directories(tls PUBLIC $ - $ ) +# FIXME (aw): check whether all of this needs to be publicly exposed target_link_libraries(tls PUBLIC OpenSSL::SSL OpenSSL::Crypto everest::evse_security + everest::staging::util + PRIVATE everest::framework ) diff --git a/lib/staging/tls/extensions/tls_types.hpp b/lib/staging/tls/extensions/tls_types.hpp index f07f73790..4dc936e33 100644 --- a/lib/staging/tls/extensions/tls_types.hpp +++ b/lib/staging/tls/extensions/tls_types.hpp @@ -6,7 +6,7 @@ #include -#include +#include struct ocsp_response_st; struct ssl_ctx_st; @@ -26,7 +26,7 @@ class StatusFlags { last = trusted_ca_keys, }; - util::AtomicEnumFlags flags; + everest::staging::util::AtomicEnumFlags flags; public: void status_request_received() { diff --git a/lib/staging/tls/openssl_util.cpp b/lib/staging/tls/openssl_util.cpp index ef3742383..2152d23fd 100644 --- a/lib/staging/tls/openssl_util.cpp +++ b/lib/staging/tls/openssl_util.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include @@ -462,6 +461,27 @@ bool signature_to_bn(bn_t& r, bn_t& s, const std::uint8_t* sig_p, std::size_t le return bRes; }; +certificate_list load_certificates_pem(const char* pem_string) { + certificate_list result{}; + if (pem_string != nullptr) { + const auto len = std::strlen(pem_string); + auto* mem = BIO_new_mem_buf(pem_string, static_cast(len)); + X509* cert = nullptr; + + while (!BIO_eof(mem)) { + if (PEM_read_bio_X509(mem, &cert, nullptr, nullptr) == nullptr) { + log_error("PEM_read_bio_X509"); + break; + } else { + result.emplace_back(certificate_ptr{cert, &X509_free}); + cert = nullptr; + } + } + BIO_free(mem); + } + return result; +} + certificate_list load_certificates(const char* filename) { certificate_list result{}; if (filename != nullptr) { @@ -592,6 +612,20 @@ std::string certificate_to_pem(const X509* cert) { return result; } +certificate_ptr pem_to_certificate(const std::string& pem) { + certificate_ptr result{nullptr, nullptr}; + auto* mem = BIO_new_mem_buf(pem.c_str(), static_cast(pem.size())); + X509* cert = nullptr; + + if (PEM_read_bio_X509(mem, &cert, nullptr, nullptr) == nullptr) { + log_error("PEM_read_bio_X509"); + } else { + result = certificate_ptr{cert, &X509_free}; + } + BIO_free(mem); + return result; +} + certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) { certificate_ptr result{nullptr, nullptr}; const auto* ptr = der; @@ -604,6 +638,18 @@ certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len) { return result; } +DER certificate_to_der(const x509_st* cert) { + assert(cert != nullptr); + + unsigned char* data{nullptr}; + + // DO NOT FREE - internal pointers to certificate + int len = i2d_X509(cert, &data); + + // move data to DER + return {der_ptr{data, &DER::free}, static_cast(len)}; +} + verify_result_t verify_certificate(const X509* cert, const certificate_list& trust_anchors, const certificate_list& untrusted) { verify_result_t result = verify_result_t::Verified; @@ -753,16 +799,9 @@ bool certificate_sha_1(openssl::sha_1_digest_t& digest, const X509* cert) { assert(cert != nullptr); bool bResult{false}; - const ASN1_BIT_STRING* signature{nullptr}; - const X509_ALGOR* alg{nullptr}; - X509_get0_signature(&signature, &alg, cert); - if (signature != nullptr) { - unsigned char* data{nullptr}; - const auto len = i2d_ASN1_BIT_STRING(signature, &data); - if (len > 0) { - bResult = openssl::sha_1(data, len, digest); - } - OPENSSL_free(data); + auto der = certificate_to_der(cert); + if (der) { + bResult = openssl::sha_1(der.get(), der.size(), digest); } return bResult; diff --git a/lib/staging/tls/openssl_util.hpp b/lib/staging/tls/openssl_util.hpp index 5df6c5f2e..7c3f0afed 100644 --- a/lib/staging/tls/openssl_util.hpp +++ b/lib/staging/tls/openssl_util.hpp @@ -335,6 +335,14 @@ DER bn_to_signature(const std::uint8_t* r, const std::uint8_t* s); */ bool signature_to_bn(openssl::bn_t& r, openssl::bn_t& s, const std::uint8_t* sig_p, std::size_t len); +/** + * \brief load any PEM encoded certificates from a string + * \param[in] pem_string + * \return a list of 0 or more certificates + * \note PEM string only supports certificates and not other PEM types + */ +certificate_list load_certificates_pem(const char* pem_string); + /** * \brief load any PEM encoded certificates from a file * \param[in] filename @@ -415,6 +423,13 @@ bool use_certificate_and_key(ssl_st* ssl, const chain_t& chain); */ std::string certificate_to_pem(const x509_st* cert); +/** + * \brief convert a PEM string to a certificate + * \param[in] pem the PEM string + * \return the certificate or empty unique_ptr on error + */ +certificate_ptr pem_to_certificate(const std::string& pem); + /** * \brief parse a DER (ASN.1) encoded certificate * \param[in] der a pointer to the DER encoded certificate @@ -423,6 +438,13 @@ std::string certificate_to_pem(const x509_st* cert); */ certificate_ptr der_to_certificate(const std::uint8_t* der, std::size_t len); +/** + * \brief encode a certificate to DER (ASN.1) + * \param[in] cert the certificate + * \return the DER encoded certificate or nullptr on error + */ +DER certificate_to_der(const x509_st* cert); + /** * \brief verify a certificate against a certificate chain and trust anchors * \param[in] cert the certificate to verify - when nullptr the certificate must @@ -463,6 +485,7 @@ pkey_ptr certificate_public_key(x509_st* cert); * \param[out] digest the SHA1 digest of the certificate * \param[in] cert the certificate * \return true on success + * \note this is the hash of the whole certificate including signature */ bool certificate_sha_1(openssl::sha_1_digest_t& digest, const x509_st* cert); @@ -476,6 +499,7 @@ bool certificate_subject_public_key_sha_1(openssl::sha_1_digest_t& digest, const enum class log_level_t : std::uint8_t { debug, + info, warning, error, }; @@ -500,6 +524,10 @@ static inline void log_debug(const std::string& str) { log(log_level_t::debug, str); } +static inline void log_info(const std::string& str) { + log(log_level_t::info, str); +} + using log_handler_t = void (*)(log_level_t level, const std::string& err); /** diff --git a/lib/staging/tls/tests/CMakeLists.txt b/lib/staging/tls/tests/CMakeLists.txt index 36c2ca2a0..00f69ef81 100644 --- a/lib/staging/tls/tests/CMakeLists.txt +++ b/lib/staging/tls/tests/CMakeLists.txt @@ -24,8 +24,7 @@ add_executable(${TLS_GTEST_NAME}) add_dependencies(${TLS_GTEST_NAME} tls_test_files_target) target_include_directories(${TLS_GTEST_NAME} PRIVATE - . .. ../../util -) + ..) target_compile_definitions(${TLS_GTEST_NAME} PRIVATE -DUNIT_TEST @@ -46,11 +45,13 @@ target_sources(${TLS_GTEST_NAME} PRIVATE ../tls.cpp ) -target_link_libraries(${TLS_GTEST_NAME} PRIVATE - GTest::gtest - OpenSSL::SSL - OpenSSL::Crypto - everest::evse_security +target_link_libraries(${TLS_GTEST_NAME} + PRIVATE + GTest::gtest + OpenSSL::SSL + OpenSSL::Crypto + everest::evse_security + everest::staging::util ) set(TLS_MAIN_NAME tls_server) @@ -58,7 +59,7 @@ add_executable(${TLS_MAIN_NAME}) add_dependencies(${TLS_MAIN_NAME} tls_test_files_target) target_include_directories(${TLS_MAIN_NAME} PRIVATE - . .. ../../util + .. ) target_compile_definitions(${TLS_MAIN_NAME} PRIVATE @@ -74,9 +75,11 @@ target_sources(${TLS_MAIN_NAME} PRIVATE ../tls.cpp ) -target_link_libraries(${TLS_MAIN_NAME} PRIVATE - OpenSSL::SSL - OpenSSL::Crypto +target_link_libraries(${TLS_MAIN_NAME} + PRIVATE + OpenSSL::SSL + OpenSSL::Crypto + everest::staging::util ) set(TLS_CLIENT_NAME tls_client) @@ -84,7 +87,7 @@ add_executable(${TLS_CLIENT_NAME}) add_dependencies(${TLS_CLIENT_NAME} tls_test_files_target) target_include_directories(${TLS_CLIENT_NAME} PRIVATE - . .. ../../util + .. ) target_compile_definitions(${TLS_CLIENT_NAME} PRIVATE @@ -100,9 +103,11 @@ target_sources(${TLS_CLIENT_NAME} PRIVATE ../tls.cpp ) -target_link_libraries(${TLS_CLIENT_NAME} PRIVATE - OpenSSL::SSL - OpenSSL::Crypto +target_link_libraries(${TLS_CLIENT_NAME} + PRIVATE + OpenSSL::SSL + OpenSSL::Crypto + everest::staging::util ) set(TLS_PATCH_NAME patched_test) @@ -110,7 +115,7 @@ add_executable(${TLS_PATCH_NAME}) add_dependencies(${TLS_PATCH_NAME} tls_test_files_target) target_include_directories(${TLS_PATCH_NAME} PRIVATE - . .. ../../util + .. ) target_compile_definitions(${TLS_PATCH_NAME} PRIVATE @@ -126,10 +131,12 @@ target_sources(${TLS_PATCH_NAME} PRIVATE ../tls.cpp ) -target_link_libraries(${TLS_PATCH_NAME} PRIVATE - GTest::gtest_main - OpenSSL::SSL - OpenSSL::Crypto +target_link_libraries(${TLS_PATCH_NAME} + PRIVATE + GTest::gtest_main + OpenSSL::SSL + OpenSSL::Crypto + everest::staging::util ) add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) diff --git a/lib/staging/tls/tests/gtest_main.cpp b/lib/staging/tls/tests/gtest_main.cpp index b73259c2a..02b6af7a3 100644 --- a/lib/staging/tls/tests/gtest_main.cpp +++ b/lib/staging/tls/tests/gtest_main.cpp @@ -17,6 +17,9 @@ void log_handler(openssl::log_level_t level, const std::string& str) { case openssl::log_level_t::debug: // std::cout << "DEBUG: " << str << std::endl; break; + case openssl::log_level_t::info: + std::cout << "INFO: " << str << std::endl; + break; case openssl::log_level_t::warning: std::cout << "WARN: " << str << std::endl; break; diff --git a/lib/staging/tls/tests/openssl_util_test.cpp b/lib/staging/tls/tests/openssl_util_test.cpp index 8ced07c31..49ad7db98 100644 --- a/lib/staging/tls/tests/openssl_util_test.cpp +++ b/lib/staging/tls/tests/openssl_util_test.cpp @@ -91,6 +91,33 @@ const char iso_exi_sig_b64[] = const char iso_exi_sig_b64_nl[] = "TI8gwUALpnYGqkgRVyovGtPBUInZVCA2NDC7JrSdsQTwjfqL+AVeY6S3Wo0xaSBv\nqNVDCLpY8FZrlrr2ks5ZUA==\n"; +const char test_cert_pem[] = "-----BEGIN CERTIFICATE-----\n" + "MIICBDCCAaqgAwIBAgIUQnMkyWtvc/a5OG8dZr9ziA5uQqYwCgYIKoZIzj0EAwIw\n" + "TjELMAkGA1UEBhMCR0IxDzANBgNVBAcMBkxvbmRvbjEPMA0GA1UECgwGUGlvbml4\n" + "MR0wGwYDVQQDDBRDUyBSb290IFRydXN0IEFuY2hvcjAeFw0yNDA5MTkxMzQwMDBa\n" + "Fw0yNDEwMjExMzQwMDBaME4xCzAJBgNVBAYTAkdCMQ8wDQYDVQQHDAZMb25kb24x\n" + "DzANBgNVBAoMBlBpb25peDEdMBsGA1UEAwwUQ1MgUm9vdCBUcnVzdCBBbmNob3Iw\n" + "WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARLkawitst5NtPoYGpDCp8/GBTDrNRJ\n" + "pCzS3KHT2lZJDOwzegRn+Zhs0csqXIQgbkCqdSozg+d83QNKcpmJk4FYo2YwZDAO\n" + "BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFB6Ytfi9uSF7NSYGXmyZEcKsWHwJMB8G\n" + "A1UdIwQYMBaAFB6Ytfi9uSF7NSYGXmyZEcKsWHwJMBIGA1UdEwEB/wQIMAYBAf8C\n" + "AQIwCgYIKoZIzj0EAwIDSAAwRQIge4+uxc2EFYD7AkHR+9d/NbULUnKFIBRLqYE+\n" + "Ib4h2CMCIQCtFWyvxwOUNidUTZGqyZXFmDyutJiNM0mi1iuFk8/8Mw==\n" + "-----END CERTIFICATE-----\n"; + +const char test_cert_hash[] = "082f891b26de97c8bdedb159f8d59113cfb55dc0"; +const char test_cert_key_hash[] = "3b094e5f2594a3ae4511a9ff4285acd91fcd11c0"; + +inline const auto to_hex_string(const openssl::sha_1_digest_t& b) { + std::stringstream string_stream; + string_stream << std::hex; + + for (int idx = 0; idx < sizeof(b); ++idx) + string_stream << std::setw(2) << std::setfill('0') << (int)b[idx]; + + return string_stream.str(); +} + TEST(util, removeHyphen) { const std::string expected{"UKSWI123456791A"}; std::string cert_emaid{"UKSWI123456791A"}; @@ -104,6 +131,24 @@ TEST(util, removeHyphen) { EXPECT_EQ(cert_emaid, expected); } +TEST(certificate_sha_1, hash) { + auto cert = openssl::pem_to_certificate(test_cert_pem); + EXPECT_TRUE(cert); + openssl::sha_1_digest_t digest; + auto res = openssl::certificate_sha_1(digest, cert.get()); + EXPECT_TRUE(res); + EXPECT_EQ(to_hex_string(digest), test_cert_hash); +} + +TEST(certificate_subject_public_key_sha_1, hash) { + auto cert = openssl::pem_to_certificate(test_cert_pem); + EXPECT_TRUE(cert); + openssl::sha_1_digest_t digest; + auto res = openssl::certificate_subject_public_key_sha_1(digest, cert.get()); + EXPECT_TRUE(res); + EXPECT_EQ(to_hex_string(digest), test_cert_key_hash); +} + TEST(DER, equal) { const std::uint8_t data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; openssl::DER a; @@ -495,6 +540,36 @@ TEST(certificate, toPem) { // std::cout << pem << std::endl; } +TEST(certificate, loadPemSingle) { + auto certs = ::openssl::load_certificates("client_ca_cert.pem"); + ASSERT_EQ(certs.size(), 1); + auto pem = ::openssl::certificate_to_pem(certs[0].get()); + EXPECT_FALSE(pem.empty()); + + auto pem_certs = ::openssl::load_certificates_pem(pem.c_str()); + ASSERT_EQ(pem_certs.size(), 1); + EXPECT_EQ(certs[0], pem_certs[0]); +} + +TEST(certificate, loadPemMulti) { + auto certs = ::openssl::load_certificates("client_chain.pem"); + ASSERT_GT(certs.size(), 1); + std::string pem; + for (const auto& cert : certs) { + pem += ::openssl::certificate_to_pem(cert.get()); + } + EXPECT_FALSE(pem.empty()); + // std::cout << pem << std::endl << "Output" << std::endl; + + auto pem_certs = ::openssl::load_certificates_pem(pem.c_str()); + ASSERT_EQ(pem_certs.size(), certs.size()); + for (auto i = 0; i < certs.size(); i++) { + SCOPED_TRACE(std::to_string(i)); + // std::cout << ::openssl::certificate_to_pem(pem_certs[i].get()) << std::endl; + EXPECT_EQ(certs[i], pem_certs[i]); + } +} + TEST(certificate, verify) { auto client = ::openssl::load_certificates("client_cert.pem"); auto chain = ::openssl::load_certificates("client_chain.pem"); diff --git a/lib/staging/tls/tests/tls_connection_test.cpp b/lib/staging/tls/tests/tls_connection_test.cpp index 9bcef8993..1361cee2a 100644 --- a/lib/staging/tls/tests/tls_connection_test.cpp +++ b/lib/staging/tls/tests/tls_connection_test.cpp @@ -741,6 +741,51 @@ TEST_F(TlsTest, TCKeysKey) { EXPECT_EQ(subject["CN"], alt_server_root_CN); } +TEST_F(TlsTest, TCKeysKeyPem) { + // same as TCKeysKey but using a PEM string trust anchor rather than file + std::map subject; + + client_config.trusted_ca_keys = true; + client_config.verify_locations_file = "alt_server_root_cert.pem"; + add_ta_key_hash("alt_server_root_cert.pem"); + + auto client_handler_fn = [this, &subject](tls::Client::ConnectionPtr& connection) { + if (connection) { + if (connection->connect() == result_t::success) { + this->set(ClientTest::flags_t::connected); + subject = openssl::certificate_subject(connection->peer_certificate()); + connection->shutdown(); + } + } + }; + + // convert file to PEM in config + for (auto& cfg : server_config.chains) { + const auto certs = ::openssl::load_certificates(cfg.trust_anchor_file); + std::string pem; + for (const auto& cert : certs) { + pem += ::openssl::certificate_to_pem(cert.get()); + } + // std::cout << cfg.trust_anchor_file << ": " << certs.size() << std::endl; + ASSERT_FALSE(pem.empty()); + cfg.trust_anchor_file = nullptr; + cfg.trust_anchor_pem = pem.c_str(); + } + + start(); + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); + + client_config.trusted_ca_keys_data.x509_name.clear(); + add_ta_key_hash("client_root_cert.pem"); + add_ta_key_hash("alt_server_root_cert.pem"); + + connect(client_handler_fn); + EXPECT_TRUE(is_set(flags_t::connected)); + EXPECT_EQ(subject["CN"], alt_server_root_CN); +} + TEST_F(TlsTest, TCKeysName) { // trusted_ca_keys - subject name matches std::map subject; diff --git a/lib/staging/tls/tests/tls_connection_test.hpp b/lib/staging/tls/tests/tls_connection_test.hpp index 83416fecd..36689661a 100644 --- a/lib/staging/tls/tests/tls_connection_test.hpp +++ b/lib/staging/tls/tests/tls_connection_test.hpp @@ -7,7 +7,6 @@ #include #include -#include #include #include #include @@ -18,6 +17,8 @@ #include #include +#include + using namespace std::chrono_literals; namespace { @@ -35,10 +36,11 @@ struct ClientStatusRequestV2Test : public ClientStatusRequestV2 { last = connected, }; - util::AtomicEnumFlags& flags; + everest::staging::util::AtomicEnumFlags& flags; ClientStatusRequestV2Test() = delete; - explicit ClientStatusRequestV2Test(util::AtomicEnumFlags& flag_ref) : flags(flag_ref) { + explicit ClientStatusRequestV2Test(everest::staging::util::AtomicEnumFlags& flag_ref) : + flags(flag_ref) { } int status_request_cb(tls::Ssl* ctx) override { @@ -96,7 +98,7 @@ struct ClientStatusRequestV2Test : public ClientStatusRequestV2 { struct ClientTest : public tls::Client { using flags_t = ClientStatusRequestV2Test::flags_t; - util::AtomicEnumFlags flags; + everest::staging::util::AtomicEnumFlags flags; ClientTest() : tls::Client(std::unique_ptr(new ClientStatusRequestV2Test(flags))) { } diff --git a/lib/staging/tls/tests/trusted_ca_keys_decode.py b/lib/staging/tls/tests/trusted_ca_keys_decode.py new file mode 100755 index 000000000..655254dec --- /dev/null +++ b/lib/staging/tls/tests/trusted_ca_keys_decode.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 + +""" +trusted ca keys hex string +"0069011d484406bca8888997a7462416445e7db117114c017f204de30f1cd42c9e6dae91b6a8ac9b8d481ba601597be7013ad6fc397b78b01d90cea1b7f909f145011d484406bca8888997a7462416445e7db117114c0100fae3900795c888a4d4d7bd9fdffa60418ac19f" + +length 0069 +"01 1d484406bca8888997a7462416445e7db117114c" +"01 7f204de30f1cd42c9e6dae91b6a8ac9b8d481ba6" +"01 597be7013ad6fc397b78b01d90cea1b7f909f145" +"01 1d484406bca8888997a7462416445e7db117114c" +"01 00fae3900795c888a4d4d7bd9fdffa60418ac19f" + +key hash from certificate +openssl x509 -in cert.pem -pubkey -noout | openssl enc -base64 -d | openssl dgst -sha1 +""" + +from cryptography import x509 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives import hashes + +import argparse + + +def certificate_key_hash(filename): + with open(filename, "rb") as fp: + cert = x509.load_pem_x509_certificate(fp.read()) + pub = cert.public_key() + pub_der = pub.public_bytes( + encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + dgst = hashes.Hash(hashes.SHA1()) + dgst.update(pub_der) + sha1 = dgst.finalize() + print(sha1.hex()) + + +def certificate_hash(filename): + # note this is the hash of the whole certificate including signature + with open(filename, "rb") as fp: + cert = x509.load_pem_x509_certificate(fp.read()) + pub_der = cert.public_bytes(encoding=serialization.Encoding.DER) + dgst = hashes.Hash(hashes.SHA1()) + dgst.update(pub_der) + sha1 = dgst.finalize() + print(sha1.hex()) + + +def trusted_ca_keys_decode(data): + data_len = int.from_bytes(data[:2], "big", signed=False) + data = data[2:] + assert len(data) == data_len + while data: + entry_type = data[0] + data = data[1:] + if entry_type == 0: + print("pre_agreed") + elif entry_type == 1: + sha1 = data[:20] + data = data[20:] + print("key_sha1_hash: %s" % sha1.hex()) + elif entry_type == 2: + print("x509_name (not decoded yet)") + elif entry_type == 3: + sha1 = data[:20] + data = data[20:] + print("cert_sha1_hash: %s" % sha1.hex()) + + +def trusted_ca_keys_decode_file(filename): + with open(filename, "rb") as fp: + trusted_ca_keys_decode(fp.read()) + + +def trusted_ca_keys_decode_hex(hexstr): + trusted_ca_keys_decode(bytes.fromhex(hexstr)) + + +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--key", + action="store", + help="filename: Print sha1 hash of certificate public key", + ) + parser.add_argument( + "--cert", action="store", help="filename: Print sha1 hash of certificate" + ) + parser.add_argument( + "--file", action="store", help="filename: Parse trusted ca keys" + ) + parser.add_argument( + "--hex", action="store", help="parse trusted ca keys hex string" + ) + + args = parser.parse_args() + if args.key: + certificate_key_hash(args.key) + if args.cert: + certificate_hash(args.cert) + if args.file: + trusted_ca_keys_decode_file(args.file) + if args.hex: + trusted_ca_keys_decode_hex(args.hex) diff --git a/lib/staging/tls/tls.cpp b/lib/staging/tls/tls.cpp index d2b0a821b..45a7b47f0 100644 --- a/lib/staging/tls/tls.cpp +++ b/lib/staging/tls/tls.cpp @@ -6,6 +6,7 @@ #include "extensions/trusted_ca_keys.hpp" #include "openssl_util.hpp" +#include #include #include #include @@ -13,8 +14,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -63,6 +66,7 @@ template <> class default_delete { } // namespace std using ::openssl::log_error; +using ::openssl::log_info; using ::openssl::log_warning; namespace { @@ -681,6 +685,10 @@ const Certificate* Connection::peer_certificate() const { return SSL_get0_peer_certificate(m_context->ctx.get()); } +SSL* Connection::ssl_context() const { + return m_context->ctx.get(); +} + // ---------------------------------------------------------------------------- // ServerConnection represents a TLS server connection @@ -688,8 +696,43 @@ std::uint32_t ServerConnection::m_count{0}; std::mutex ServerConnection::m_cv_mutex; std::condition_variable ServerConnection::m_cv; +namespace { + +int ssl_keylog_file_index{-1}; +int ssl_keylog_server_index{-1}; + +void keylog_callback(const SSL* ssl, const char* line) { + + auto keylog_server = static_cast(SSL_get_ex_data(ssl, ssl_keylog_server_index)); + + std::string key_log_msg = "TLS Handshake keys on port "; + key_log_msg += std::to_string(keylog_server->get_port()) + ": "; + key_log_msg += std::string(line); + + log_info(key_log_msg); + + if (keylog_server->get_fd() != -1) { + const auto result = keylog_server->send(line); + if (result not_eq strlen(line)) { + log_error("key_logging_server send() failed!"); + } + } + + auto keylog_file_path = + static_cast(SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), ssl_keylog_file_index)); + + if (not keylog_file_path->empty()) { + std::ofstream ofs; + ofs.open(keylog_file_path->string(), std::ofstream::out | std::ofstream::app); + ofs << line << std::endl; + ofs.close(); + } +} + +} // namespace + ServerConnection::ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, - std::int32_t timeout_ms) : + std::int32_t timeout_ms, const ConfigItem& tls_key_interface) : Connection(ctx, soc, ip_in, service_in, timeout_ms), m_tck_data{m_trusted_ca_keys, m_flags} { { std::lock_guard lock(m_cv_mutex); @@ -699,6 +742,12 @@ ServerConnection::ServerConnection(SslContext* ctx, int soc, const char* ip_in, SSL_set_accept_state(m_context->ctx.get()); ServerStatusRequestV2::set_data(m_context->ctx.get(), &m_flags); ServerTrustedCaKeys::set_data(m_context->ctx.get(), &m_tck_data); + + if (tls_key_interface != nullptr) { + const auto port = std::stoul(service_in); + m_keylog_server = std::make_unique(std::string(tls_key_interface), port); + SSL_set_ex_data(m_context->ctx.get(), ssl_keylog_server_index, m_keylog_server.get()); + } } } @@ -881,6 +930,26 @@ bool Server::init_ssl(const config_t& cfg) { // use the first server chain result = configure_ssl_ctx(ctx, cfg.ciphersuites, cfg.cipher_list, cfg.chains[0], true); if (result) { + + if (cfg.tls_key_logging) { + tls_key_log_file_path = std::filesystem::path(cfg.tls_key_logging_path) /= "tls_session_keys.log"; + + ssl_keylog_file_index = SSL_CTX_get_ex_new_index(0, std::string("").data(), nullptr, nullptr, nullptr); + ssl_keylog_server_index = SSL_get_ex_new_index(0, std::string("").data(), nullptr, nullptr, nullptr); + + if (ssl_keylog_file_index == -1 or ssl_keylog_server_index == -1) { + auto error_msg = std::string("_get_ex_new_index failed: ssl_keylog_file_index: "); + error_msg += std::to_string(ssl_keylog_file_index); + error_msg += ", ssl_keylog_server_index: " + std::to_string(ssl_keylog_server_index); + log_error(error_msg); + } else { + SSL_CTX_set_ex_data(ctx, ssl_keylog_file_index, &tls_key_log_file_path); + + SSL_CTX_set_keylog_callback(ctx, keylog_callback); + m_tls_key_interface = cfg.host; + } + } + int mode = SSL_VERIFY_NONE; // TODO(james-ctc): verify may need to change based on TLS version @@ -930,8 +999,12 @@ bool Server::init_certificates(const std::vector& chain_fi for (const auto& i : chain_files) { auto certs = openssl::load_certificates(i.certificate_chain_file); auto tas = openssl::load_certificates(i.trust_anchor_file); + auto tas_pem = openssl::load_certificates_pem(i.trust_anchor_pem); auto pkey = openssl::load_private_key(i.private_key_file, i.private_key_password); + // combine all trust anchor certificates + std::move(tas_pem.begin(), tas_pem.end(), std::back_inserter(tas)); + if (certs.size() > 0) { openssl::chain_t chain; @@ -1054,8 +1127,9 @@ void Server::wait_for_connection(const ConnectionHandler& handler) { // new connection, pass to handler auto* ip = BIO_ADDR_hostname_string(peer.get(), 1); auto* service = BIO_ADDR_service_string(peer.get(), 1); - auto connection = - std::make_unique(m_context->ctx.get(), soc, ip, service, m_timeout_ms); + + auto connection = std::make_unique(m_context->ctx.get(), soc, ip, service, + m_timeout_ms, m_tls_key_interface); handler(std::move(connection)); OPENSSL_free(ip); OPENSSL_free(service); @@ -1337,4 +1411,81 @@ Client::override_t Client::default_overrides() { }; } +// ---------------------------------------------------------------------------- +// TlsKeyLoggingServer + +TlsKeyLoggingServer::TlsKeyLoggingServer(const std::string& interface_name, uint16_t port_) : port(port_) { + static constexpr auto LINK_LOCAL_MULTICAST = "ff02::1"; + bool result{true}; + + fd = socket(AF_INET6, SOCK_DGRAM, 0); + if (fd == -1) { + log_error("Could not create socket"); + result = false; + } + + if (result) { + // source setup + // find port between 49152-65535 + auto could_bind = false; + auto source_port = 49152; + for (; source_port < 65535; source_port++) { + sockaddr_in6 source_address = {AF_INET6, htons(source_port), 0, {}, 0}; + if (bind(fd, reinterpret_cast(&source_address), sizeof(sockaddr_in6)) == 0) { + could_bind = true; + break; + } + } + + if (could_bind) { + log_info("UDP socket bound to source port: " + std::to_string(source_port)); + } else { + log_error("Could not bind"); + result = false; + } + } + + if (result) { + auto mreq = ipv6_mreq{}; + const auto index = if_nametoindex(interface_name.c_str()); + mreq.ipv6mr_interface = index; + if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &mreq.ipv6mr_multiaddr) <= 0) { + log_error("Failed to setup multicast address"); + result = false; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + log_error("Could not add multicast group membership"); + result = false; + } + + if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &index, sizeof(index)) < 0) { + log_error("Could not set interface name:" + interface_name); + result = false; + } + + destination_address = {AF_INET6, htons(port), 0, {}, 0}; + if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &destination_address.sin6_addr) <= 0) { + log_error("Failed to setup server address, reset key_log_fd"); + result = false; + } + } + + if (!result && fd != -1) { + close(fd); + fd = -1; + } +} + +TlsKeyLoggingServer::~TlsKeyLoggingServer() { + if (fd != -1) { + close(fd); + } +} + +ssize_t TlsKeyLoggingServer::send(const char* line) { + return sendto(fd, line, strlen(line), 0, reinterpret_cast(&destination_address), + sizeof(destination_address)); +} + } // namespace tls diff --git a/lib/staging/tls/tls.hpp b/lib/staging/tls/tls.hpp index aa838010e..f48de624e 100644 --- a/lib/staging/tls/tls.hpp +++ b/lib/staging/tls/tls.hpp @@ -12,9 +12,12 @@ #include #include #include +#include #include #include #include +#include +#include #include #include #include @@ -56,6 +59,27 @@ class ConfigItem { } }; +class TlsKeyLoggingServer { +public: + TlsKeyLoggingServer(const std::string& interface_name, uint16_t port_); + ~TlsKeyLoggingServer(); + + ssize_t send(const char* line); + + auto get_fd() const { + return fd; + } + + auto get_port() const { + return port; + } + +private: + int fd{-1}; + uint16_t port{0}; + sockaddr_in6 destination_address{}; +}; + // ---------------------------------------------------------------------------- // Connection represents a TLS connection @@ -221,6 +245,23 @@ class Connection { * \note the certificate must not be freed */ [[nodiscard]] const Certificate* peer_certificate() const; + + /** + * \brief obtain the underlying SSL context + * \returns the underlying SSL context pointer + */ + [[nodiscard]] [[deprecated( + "Temporarily used with IsoMux module. Will be removed together with IsoMux module in the future.")]] SSL* + ssl_context() const; + + /** + * \brief set the read timeout in ms + */ + [[deprecated( + "Temporarily used with IsoMux module. Will be removed together with IsoMux module in the future.")]] void + set_read_timeout(int ms) { + m_timeout_ms = ms; + } }; /** @@ -240,8 +281,11 @@ class ServerConnection : public Connection { StatusFlags m_flags; //!< extension flags server_trusted_ca_keys_t m_tck_data; //!< extension per connection data + std::unique_ptr m_keylog_server{nullptr}; + public: - ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms); + ServerConnection(SslContext* ctx, int soc, const char* ip_in, const char* service_in, std::int32_t timeout_ms, + const ConfigItem& tls_key_interface); ServerConnection() = delete; ServerConnection(const ServerConnection&) = delete; ServerConnection(ServerConnection&&) = delete; @@ -341,6 +385,7 @@ class Server { //!< server certificate is the first certificate in the file followed by any intermediate CAs ConfigItem certificate_chain_file{nullptr}; ConfigItem trust_anchor_file{nullptr}; //!< one or more trust anchor PEM certificates + ConfigItem trust_anchor_pem{nullptr}; //!< one or more trust anchor PEM certificates ConfigItem private_key_file{nullptr}; //!< key associated with the server certificate ConfigItem private_key_password{nullptr}; //!< optional password to read private key std::vector ocsp_response_files; //!< list of OCSP files in certificate chain order @@ -362,6 +407,9 @@ class Server { ConfigItem service{nullptr}; //!< TLS port number as a string int socket{INVALID_SOCKET}; //!< use this specific socket - bypasses socket setup in init_socket() when set bool ipv6_only{true}; //!< listen on IPv6 only, when false listen on IPv4 only + + bool tls_key_logging{false}; //!< tls key logging is active when true + std::string tls_key_logging_path; //!< tls key logging file path }; using ConnectionPtr = std::unique_ptr; @@ -390,6 +438,9 @@ class Server { static int s_sig_int; //!< signal to use to wakeup serve() ConfigurationCallback m_init_callback{nullptr}; //!< callback to retrieve SSL configuration + ConfigItem m_tls_key_interface{nullptr}; + std::filesystem::path tls_key_log_file_path{}; + /** * \brief initialise the server socket * \param[in] cfg server configuration diff --git a/lib/staging/util/BUILD.bazel b/lib/staging/util/BUILD.bazel new file mode 100644 index 000000000..c235560ca --- /dev/null +++ b/lib/staging/util/BUILD.bazel @@ -0,0 +1,6 @@ +cc_library( + name = "util", + hdrs = ["include/*.hpp"], + visibility = ["//visibility:public"], + includes = ["include"], +) \ No newline at end of file diff --git a/lib/staging/util/CMakeLists.txt b/lib/staging/util/CMakeLists.txt new file mode 100644 index 000000000..9ceb4c91f --- /dev/null +++ b/lib/staging/util/CMakeLists.txt @@ -0,0 +1,11 @@ +add_library(everest_staging_util INTERFACE) +add_library(everest::staging::util ALIAS everest_staging_util) + +target_include_directories(everest_staging_util + INTERFACE + $ +) + +if (BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/lib/staging/util/EnumFlags.hpp b/lib/staging/util/include/everest/staging/util/EnumFlags.hpp similarity index 94% rename from lib/staging/util/EnumFlags.hpp rename to lib/staging/util/include/everest/staging/util/EnumFlags.hpp index e4f80788b..14fb72452 100644 --- a/lib/staging/util/EnumFlags.hpp +++ b/lib/staging/util/include/everest/staging/util/EnumFlags.hpp @@ -7,7 +7,7 @@ #include #include -namespace util { +namespace everest::staging::util { template class AtomicEnumFlags { static_assert(std::is_enum(), "Not enum"); @@ -53,5 +53,5 @@ template class AtomicEnumFlags { } }; -} // namespace util +} // namespace everest::staging::util #endif diff --git a/lib/staging/util/tests/CMakeLists.txt b/lib/staging/util/tests/CMakeLists.txt new file mode 100644 index 000000000..97c0e2e06 --- /dev/null +++ b/lib/staging/util/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +add_executable(EnumFlagsTest + EnumFlagsTest.cpp +) + +target_link_libraries(EnumFlagsTest + PRIVATE + GTest::gtest_main + everest::staging::util +) + +include(GoogleTest) +gtest_discover_tests(EnumFlagsTest) diff --git a/modules/EvseManager/tests/EnumFlagsTest.cpp b/lib/staging/util/tests/EnumFlagsTest.cpp similarity index 65% rename from modules/EvseManager/tests/EnumFlagsTest.cpp rename to lib/staging/util/tests/EnumFlagsTest.cpp index d7437842c..6cf07e04d 100644 --- a/modules/EvseManager/tests/EnumFlagsTest.cpp +++ b/lib/staging/util/tests/EnumFlagsTest.cpp @@ -1,17 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest -#include #include -#include - -namespace { +#include enum class ErrorHandlingFlags : std::uint8_t { prevent_charging, prevent_charging_welded, all_errors_cleared, - last = AllErrorCleared + last = all_errors_cleared }; enum class BspErrors : std::uint8_t { @@ -39,52 +36,52 @@ enum class BspErrors : std::uint8_t { last = VendorError }; +using namespace everest::staging::util; + TEST(AtomicEnumFlagsTest, init) { - module::AtomicEnumFlags flags; + AtomicEnumFlags flags; EXPECT_TRUE(flags.all_reset()); } TEST(AtomicEnumFlagsTest, init_large) { - module::AtomicEnumFlags flags; + AtomicEnumFlags flags; EXPECT_TRUE(flags.all_reset()); } TEST(AtomicEnumFlagsTest, set_reset_one) { - module::AtomicEnumFlags flags; + AtomicEnumFlags flags; EXPECT_TRUE(flags.all_reset()); - flags.set(ErrorHandlingFlags::AllErrorCleared); + flags.set(ErrorHandlingFlags::all_errors_cleared); EXPECT_FALSE(flags.all_reset()); - flags.reset(ErrorHandlingFlags::AllErrorCleared); + flags.reset(ErrorHandlingFlags::all_errors_cleared); EXPECT_TRUE(flags.all_reset()); } TEST(AtomicEnumFlagsTest, set_reset_two) { - module::AtomicEnumFlags flags; + AtomicEnumFlags flags; EXPECT_TRUE(flags.all_reset()); - flags.set(ErrorHandlingFlags::AllErrorCleared); + flags.set(ErrorHandlingFlags::all_errors_cleared); EXPECT_FALSE(flags.all_reset()); - flags.set(ErrorHandlingFlags::PreventCharging); + flags.set(ErrorHandlingFlags::prevent_charging); EXPECT_FALSE(flags.all_reset()); - flags.reset(ErrorHandlingFlags::AllErrorCleared); + flags.reset(ErrorHandlingFlags::all_errors_cleared); EXPECT_FALSE(flags.all_reset()); - flags.reset(ErrorHandlingFlags::PreventCharging); + flags.reset(ErrorHandlingFlags::prevent_charging); EXPECT_TRUE(flags.all_reset()); } TEST(AtomicEnumFlagsTest, set_reset_three) { - module::AtomicEnumFlags flags; + AtomicEnumFlags flags; EXPECT_TRUE(flags.all_reset()); - flags.set(ErrorHandlingFlags::AllErrorCleared); + flags.set(ErrorHandlingFlags::all_errors_cleared); EXPECT_FALSE(flags.all_reset()); - flags.set(ErrorHandlingFlags::PreventCharging); + flags.set(ErrorHandlingFlags::prevent_charging); EXPECT_FALSE(flags.all_reset()); flags.set(ErrorHandlingFlags::prevent_charging_welded); EXPECT_FALSE(flags.all_reset()); flags.reset(); EXPECT_TRUE(flags.all_reset()); } - -} // namespace diff --git a/module-dependencies.cmake b/module-dependencies.cmake index a252a2857..de3c28095 100644 --- a/module-dependencies.cmake +++ b/module-dependencies.cmake @@ -6,10 +6,6 @@ ev_define_dependency( DEPENDENCY_NAME sigslot DEPENDENT_MODULES_LIST EnergyNode EvseManager MicroMegaWattBSP YetiDriver) -ev_define_dependency( - DEPENDENCY_NAME libmodbus - DEPENDENT_MODULES_LIST PowermeterBSM) - ev_define_dependency( DEPENDENCY_NAME pugixml DEPENDENT_MODULES_LIST EvseManager) @@ -57,6 +53,11 @@ ev_define_dependency( DEPENDENCY_NAME sqlite_cpp DEPENDENT_MODULES_LIST ErrorHistory) +ev_define_dependency( + DEPENDENCY_NAME libiso15118 + OUTPUT_VARIABLE_SUFFIX LIBISO15118 + DEPENDENT_MODULES_LIST Evse15118D20) + if(NOT everest-gpio IN_LIST EVEREST_EXCLUDE_DEPENDENCIES) set(EVEREST_DEPENDENCY_ENABLED_EVEREST_GPIO ON) else() diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 361b179ba..fd56c74b6 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest #include "API.hpp" +#include #include #include @@ -265,16 +266,36 @@ SessionInfo::operator std::string() { void API::init() { invoke_init(*p_main); + + // ensure all evse_energy_sink(s) that are connected have an evse id mapping + for (const auto& evse_sink : this->r_evse_energy_sink) { + if (not evse_sink->get_mapping().has_value()) { + EVLOG_critical << "Please configure an evse mapping your configuration file for the connected " + "r_evse_energy_sink with module_id: " + << evse_sink->module_id; + throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse."); + } + } + this->limit_decimal_places = std::make_unique(this->config); std::vector connectors; std::string var_connectors = this->api_base + "connectors"; + evse_manager_check.set_total(r_evse_manager.size()); + + int evse_id = 1; for (auto& evse : this->r_evse_manager) { auto& session_info = this->info.emplace_back(std::make_unique()); auto& hw_caps = this->hw_capabilities_str.emplace_back(""); std::string evse_base = this->api_base + evse->module_id; connectors.push_back(evse->module_id); + evse->subscribe_ready([this, &evse](bool ready) { + if (ready) { + this->evse_manager_check.notify_ready(evse->module_id); + } + }); + // API variables std::string var_base = evse_base + "/var/"; @@ -395,7 +416,7 @@ void API::init() { std::string cmd_base = evse_base + "/cmd/"; std::string cmd_enable_disable = cmd_base + "enable_disable"; - this->mqtt.subscribe(cmd_enable_disable, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_enable_disable, [this, &evse](const std::string& data) { auto connector_id = 0; types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, types::evse_manager::Enable_state::Enable, 100}; @@ -422,11 +443,12 @@ void API::init() { } else { EVLOG_error << "enable: No argument specified, ignoring command"; } + this->evse_manager_check.wait_ready(); evse->call_enable_disable(connector_id, enable_source); }); std::string cmd_disable = cmd_base + "disable"; - this->mqtt.subscribe(cmd_disable, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_disable, [this, &evse](const std::string& data) { auto connector_id = 0; types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, types::evse_manager::Enable_state::Disable, 100}; @@ -441,11 +463,12 @@ void API::init() { } else { EVLOG_error << "disable: No argument specified, ignoring command"; } + this->evse_manager_check.wait_ready(); evse->call_enable_disable(connector_id, enable_source); }); std::string cmd_enable = cmd_base + "enable"; - this->mqtt.subscribe(cmd_enable, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_enable, [this, &evse](const std::string& data) { auto connector_id = 0; types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, types::evse_manager::Enable_state::Enable, 100}; @@ -460,44 +483,58 @@ void API::init() { } else { EVLOG_error << "disable: No argument specified, ignoring command"; } + this->evse_manager_check.wait_ready(); evse->call_enable_disable(connector_id, enable_source); }); std::string cmd_pause_charging = cmd_base + "pause_charging"; - this->mqtt.subscribe(cmd_pause_charging, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_pause_charging, [this, &evse](const std::string& data) { + this->evse_manager_check.wait_ready(); evse->call_pause_charging(); // }); std::string cmd_resume_charging = cmd_base + "resume_charging"; - this->mqtt.subscribe(cmd_resume_charging, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_resume_charging, [this, &evse](const std::string& data) { + this->evse_manager_check.wait_ready(); evse->call_resume_charging(); // }); std::string cmd_set_limit = cmd_base + "set_limit_amps"; - this->mqtt.subscribe(cmd_set_limit, [&evse](const std::string& data) { - try { - const auto external_limits = get_external_limits(data, false); - evse->call_set_external_limits(external_limits); - } catch (const std::invalid_argument& e) { - EVLOG_warning << "Invalid limit: No conversion of given input could be performed."; - } catch (const std::out_of_range& e) { - EVLOG_warning << "Invalid limit: Out of range."; - } - }); - std::string cmd_set_limit_watts = cmd_base + "set_limit_watts"; - this->mqtt.subscribe(cmd_set_limit_watts, [&evse](const std::string& data) { - try { - const auto external_limits = get_external_limits(data, true); - evse->call_set_external_limits(external_limits); - } catch (const std::invalid_argument& e) { - EVLOG_warning << "Invalid limit: No conversion of given input could be performed."; - } catch (const std::out_of_range& e) { - EVLOG_warning << "Invalid limit: Out of range."; - } - }); + if (external_energy_limits::is_evse_sink_configured(this->r_evse_energy_sink, evse_id)) { + auto& evse_energy_sink = + external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, evse_id); + + this->mqtt.subscribe(cmd_set_limit, [&evse_manager_check = this->evse_manager_check, + &evse_energy_sink = evse_energy_sink](const std::string& data) { + try { + const auto external_limits = get_external_limits(data, false); + evse_manager_check.wait_ready(); + evse_energy_sink.call_set_external_limits(external_limits); + } catch (const std::invalid_argument& e) { + EVLOG_warning << "Invalid limit: No conversion of given input could be performed."; + } + }); + + std::string cmd_set_limit_watts = cmd_base + "set_limit_watts"; + + this->mqtt.subscribe(cmd_set_limit_watts, [&evse_manager_check = this->evse_manager_check, + &evse_energy_sink = evse_energy_sink](const std::string& data) { + try { + const auto external_limits = get_external_limits(data, true); + evse_manager_check.wait_ready(); + evse_energy_sink.call_set_external_limits(external_limits); + } catch (const std::invalid_argument& e) { + EVLOG_warning << "Invalid limit: No conversion of given input could be performed."; + } + }); + } else { + EVLOG_warning << "No evse energy sink configured for evse_id: " << evse_id + << ". API module does therefore not allow control of amps or power limits for this EVSE"; + } + std::string cmd_force_unlock = cmd_base + "force_unlock"; - this->mqtt.subscribe(cmd_force_unlock, [&evse](const std::string& data) { + this->mqtt.subscribe(cmd_force_unlock, [this, &evse](const std::string& data) { int connector_id = 1; if (!data.empty()) { try { @@ -512,6 +549,7 @@ void API::init() { // perform the same action types::evse_manager::StopTransactionRequest req; req.reason = types::evse_manager::StopTransactionReason::UnlockCommand; + this->evse_manager_check.wait_ready(); evse->call_stop_transaction(req); evse->call_force_unlock(connector_id); }); @@ -549,6 +587,7 @@ void API::init() { }); } } + evse_id++; } std::string var_ocpp_connection_status = this->api_base + "ocpp/var/connection_status"; diff --git a/modules/API/API.hpp b/modules/API/API.hpp index 5aa7b1329..d4ea8880d 100644 --- a/modules/API/API.hpp +++ b/modules/API/API.hpp @@ -16,11 +16,13 @@ // headers for required interface implementations #include #include +#include #include #include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here +#include #include #include #include @@ -29,6 +31,7 @@ #include #include +#include "StartupMonitor.hpp" #include "limit_decimal_places.hpp" namespace module { @@ -155,7 +158,8 @@ class API : public Everest::ModuleBase { API(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr p_main, std::vector> r_evse_manager, std::vector> r_ocpp, std::vector> r_random_delay, - std::vector> r_error_history, Conf& config) : + std::vector> r_error_history, + std::vector> r_evse_energy_sink, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), @@ -163,6 +167,7 @@ class API : public Everest::ModuleBase { r_ocpp(std::move(r_ocpp)), r_random_delay(std::move(r_random_delay)), r_error_history(std::move(r_error_history)), + r_evse_energy_sink(std::move(r_evse_energy_sink)), config(config){}; Everest::MqttProvider& mqtt; @@ -171,6 +176,7 @@ class API : public Everest::ModuleBase { const std::vector> r_ocpp; const std::vector> r_random_delay; const std::vector> r_error_history; + const std::vector> r_evse_energy_sink; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -192,6 +198,8 @@ class API : public Everest::ModuleBase { std::vector api_threads; bool running = true; + StartupMonitor evse_manager_check; + std::list> info; std::list hw_capabilities_str; std::string selected_protocol; diff --git a/modules/API/CMakeLists.txt b/modules/API/CMakeLists.txt index 7146700eb..d1da6d21d 100644 --- a/modules/API/CMakeLists.txt +++ b/modules/API/CMakeLists.txt @@ -12,10 +12,12 @@ ev_setup_cpp_module() target_link_libraries(${MODULE_NAME} PRIVATE ryml::ryml + everest::external_energy_limits ) target_sources(${MODULE_NAME} PRIVATE "limit_decimal_places.cpp" + "StartupMonitor.cpp" ) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 @@ -26,4 +28,7 @@ target_sources(${MODULE_NAME} # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 # insert other things like install cmds etc here +if(EVEREST_CORE_BUILD_TESTING) + add_subdirectory(tests) +endif() # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/API/README.md b/modules/API/README.md index 1ec7439be..afb4c211b 100644 --- a/modules/API/README.md +++ b/modules/API/README.md @@ -216,8 +216,8 @@ It will actually call the following command on everest_api/evse_manager/cmd/enab { "connector_id": 1, "source": "LocalAPI", - "state": "enable", - "priority": 0 + "state": "Enable", + "priority": 100 } ``` @@ -230,8 +230,8 @@ It will actually call the following command on everest_api/evse_manager/cmd/enab { "connector_id": 1, "source": "LocalAPI", - "state": "disable", - "priority": 0 + "state": "Disable", + "priority": 100 } ``` @@ -244,9 +244,13 @@ If any arbitrary payload is published to this topic charging will be paused by t ### everest_api/evse_manager/cmd/set_limit_amps Command to set an amps limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number. +📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic! + ### everest_api/evse_manager/cmd/set_limit_watts Command to set a watt limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number. +📌 **Note:** You have to configure one evse_energy_sink connection per EVSE within the configuration file in order to use this topic! + ### everest_api/evse_manager/cmd/force_unlock Command to force unlock a connector on the EVSE. They payload should be a positive integer identifying the connector that should be unlocked. If the payload is empty or cannot be converted to an integer connector 1 is assumed. diff --git a/modules/API/StartupMonitor.cpp b/modules/API/StartupMonitor.cpp new file mode 100644 index 000000000..7799f3b69 --- /dev/null +++ b/modules/API/StartupMonitor.cpp @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "StartupMonitor.hpp" +#include + +#include + +namespace module { + +bool StartupMonitor::check_ready() { + bool result{false}; + if (ready_set) { + result = ready_set->size() >= n_managers; + } + return result; +} + +bool StartupMonitor::set_total(std::uint8_t total) { + bool result{true}; + { + std::lock_guard lock(mutex); + if (!ready_set) { + n_managers = total; + if (total == 0) { + managers_ready = true; + } else { + managers_ready = false; + ready_set = std::make_unique(); + } + } else { + // already set + EVLOG_error << "Invalid attempt to set number of EVSE managers"; + result = false; + } + } + if (total == 0) { + cv.notify_all(); + } + return result; +} + +void StartupMonitor::wait_ready() { + std::unique_lock lock(mutex); + cv.wait(lock, [this] { return this->managers_ready; }); +} + +bool StartupMonitor::notify_ready(const std::string& evse_manager_id) { + bool result{true}; + bool notify{false}; + { + std::lock_guard lock(mutex); + if (ready_set) { + ready_set->insert(evse_manager_id); + notify = StartupMonitor::check_ready(); + if (notify) { + managers_ready = true; + n_managers = 0; + ready_set->clear(); // reclaim memory + } + } else { + result = false; + if (managers_ready) { + EVLOG_warning << "EVSE manager ready after complete"; + } else { + EVLOG_error << "EVSE manager ready before total number set"; + } + } + } + if (notify) { + cv.notify_all(); + } + return result; +} + +} // namespace module diff --git a/modules/API/StartupMonitor.hpp b/modules/API/StartupMonitor.hpp new file mode 100644 index 000000000..023552001 --- /dev/null +++ b/modules/API/StartupMonitor.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef STARTUPMONITOR_HPP +#define STARTUPMONITOR_HPP + +#include +#include +#include +#include +#include +#include + +namespace module { + +/** + * \brief collect ready responses from all EVSE managers + * + * Provides a mechanism for API code to wait for all EVSE managers to be ready. + * Every EVSE manager is expected to set a `ready` variable to true. This class + * collects the IDs of EVSE managers to check that the expected number are + * ready before allowing API calls to proceed. + * + * \note an EVSE manager is not expected to set `ready` more than once, however + * this class manages this so that the `ready` is only counted once. + */ +class StartupMonitor { +private: + using ready_t = std::set; + + std::condition_variable cv; + std::mutex mutex; + +protected: + std::unique_ptr ready_set; //!< set of received ready responses + std::uint16_t n_managers{0}; //!< total number of EVSE managers + bool managers_ready{false}; //!< all EVSE managers are ready + + /** + * \brief check whether all ready responses have been received + * \returns true when the ready set contains at least n_managers responses + */ + bool check_ready(); + +public: + /** + * \brief set the total number of EVSE managers + * \param[in] total the number of EVSE managers + * \returns false if the total has already been set + */ + bool set_total(std::uint8_t total); + + /** + * \brief wait for all EVSE managers to be ready + */ + void wait_ready(); + + /** + * \brief notify that a specific EVSE manager is ready + * \param[in] evse_manager_id the ID of the EVSE manager + * \returns false if the total has not been set + * \note notify_ready() may be called multiple times with the same evse_manager_id + */ + bool notify_ready(const std::string& evse_manager_id); +}; + +} // namespace module + +#endif // STARTUPMONITOR_HPP diff --git a/modules/API/manifest.yaml b/modules/API/manifest.yaml index ec1b0e9fb..ff5ad813d 100644 --- a/modules/API/manifest.yaml +++ b/modules/API/manifest.yaml @@ -189,6 +189,10 @@ requires: interface: error_history min_connections: 0 max_connections: 1 + evse_energy_sink: + interface: external_energy_limits + min_connections: 0 + max_connections: 128 enable_external_mqtt: true metadata: license: https://opensource.org/licenses/Apache-2.0 diff --git a/modules/API/tests/CMakeLists.txt b/modules/API/tests/CMakeLists.txt new file mode 100644 index 000000000..b3519bf91 --- /dev/null +++ b/modules/API/tests/CMakeLists.txt @@ -0,0 +1,19 @@ +set(TEST_TARGET_NAME ${PROJECT_NAME}_API_tests) +add_executable(${TEST_TARGET_NAME}) + +add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME}) + +target_include_directories(${TEST_TARGET_NAME} PRIVATE + . .. ../../../tests/include +) + +target_sources(${TEST_TARGET_NAME} PRIVATE + StartupMonitor_test.cpp + ../StartupMonitor.cpp +) + +target_link_libraries(${TEST_TARGET_NAME} PRIVATE + GTest::gtest_main +) + +add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) diff --git a/modules/API/tests/StartupMonitor_test.cpp b/modules/API/tests/StartupMonitor_test.cpp new file mode 100644 index 000000000..55ddec705 --- /dev/null +++ b/modules/API/tests/StartupMonitor_test.cpp @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include + +#include "StartupMonitor.hpp" + +#include + +namespace { +using namespace module; + +struct StartupMonitorTest : public StartupMonitor { + [[nodiscard]] constexpr bool startup_complete() const { + return managers_ready; + } + [[nodiscard]] constexpr std::uint8_t total() const { + return n_managers; + } + [[nodiscard]] inline std::uint8_t startup_count() const { + return (ready_set) ? ready_set->size() : 0; + } +}; + +TEST(StartupMonitor, init) { + StartupMonitorTest startup; + EXPECT_FALSE(startup.startup_complete()); + EXPECT_EQ(startup.startup_count(), 0); + EXPECT_EQ(startup.total(), 0); + + bool woken{false}; + std::thread thread([&startup, &woken]() { + startup.wait_ready(); + woken = true; + }); + + EXPECT_FALSE(woken); + + EXPECT_TRUE(startup.set_total(1)); + EXPECT_EQ(startup.total(), 1); + EXPECT_FALSE(woken); + EXPECT_TRUE(startup.notify_ready("manager1")); + // EXPECT_EQ(startup.startup_count(), 1); will be 0 because startup is complete + thread.join(); + EXPECT_TRUE(woken); + EXPECT_TRUE(startup.startup_complete()); + EXPECT_EQ(startup.total(), 0); +} + +TEST(StartupMonitor, zero) { + StartupMonitorTest startup; + EXPECT_FALSE(startup.startup_complete()); + EXPECT_EQ(startup.startup_count(), 0); + EXPECT_EQ(startup.total(), 0); + + bool woken{false}; + std::thread thread([&startup, &woken]() { + startup.wait_ready(); + woken = true; + }); + + EXPECT_FALSE(woken); + + EXPECT_TRUE(startup.set_total(0)); + EXPECT_EQ(startup.total(), 0); + EXPECT_EQ(startup.startup_count(), 0); + thread.join(); + EXPECT_TRUE(woken); + EXPECT_TRUE(startup.startup_complete()); + EXPECT_EQ(startup.total(), 0); +} + +TEST(StartupMonitor, invalidSequence) { + StartupMonitorTest startup; + EXPECT_FALSE(startup.startup_complete()); + EXPECT_FALSE(startup.notify_ready("manager1")); // total not set yet + EXPECT_TRUE(startup.set_total(1)); + EXPECT_EQ(startup.startup_count(), 0); + EXPECT_EQ(startup.total(), 1); + + bool woken{false}; + std::thread thread([&startup, &woken]() { + startup.wait_ready(); + woken = true; + }); + + EXPECT_FALSE(startup.set_total(2)); // total already set + EXPECT_EQ(startup.total(), 1); // didn't change + EXPECT_TRUE(startup.notify_ready("manager2")); + // EXPECT_EQ(startup.startup_count(), 1); will be 0 because startup is complete + thread.join(); + EXPECT_TRUE(woken); + EXPECT_TRUE(startup.startup_complete()); + EXPECT_EQ(startup.total(), 0); +} + +TEST(StartupMonitor, duplicateReady) { + StartupMonitorTest startup; + EXPECT_FALSE(startup.startup_complete()); + EXPECT_TRUE(startup.set_total(2)); + EXPECT_EQ(startup.startup_count(), 0); + EXPECT_EQ(startup.total(), 2); + + bool woken{false}; + std::thread thread([&startup, &woken]() { + startup.wait_ready(); + woken = true; + }); + + EXPECT_TRUE(startup.notify_ready("manager1")); + EXPECT_EQ(startup.startup_count(), 1); + EXPECT_TRUE(startup.notify_ready("manager1")); // duplicate + EXPECT_EQ(startup.startup_count(), 1); + EXPECT_FALSE(startup.startup_complete()); + EXPECT_TRUE(startup.notify_ready("manager2")); + // EXPECT_EQ(startup.startup_count(), 2); will be 0 because startup is complete + EXPECT_TRUE(startup.startup_complete()); + + thread.join(); + EXPECT_TRUE(woken); + EXPECT_TRUE(startup.startup_complete()); + EXPECT_EQ(startup.total(), 0); +} + +} // namespace diff --git a/modules/Auth/Auth.cpp b/modules/Auth/Auth.cpp index e08072038..c4f90d078 100644 --- a/modules/Auth/Auth.cpp +++ b/modules/Auth/Auth.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest +#include #include @@ -14,7 +15,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 +32,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 +63,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 +81,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() && evse_id.value() > 0) { + 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 bcbd73898..e40502766 100644 --- a/modules/Auth/BUILD.bazel +++ b/modules/Auth/BUILD.bazel @@ -11,6 +11,16 @@ cc_library( "@everest-framework//:framework", "@com_github_HowardHinnant_date//:date", "//types:types_lib", + "//interfaces:interfaces_lib", + "//lib/staging/helpers", + ], + # See https://github.com/HowardHinnant/date/issues/324 + local_defines = [ + "BUILD_TZ_LIB=ON", + "USE_SYSTEM_TZ_DB=ON", + "USE_OS_TZDB=1", + "USE_AUTOLOAD=0", + "HAS_REMOTE_API=0", ], copts = ["-std=c++17"], ) diff --git a/modules/Auth/CMakeLists.txt b/modules/Auth/CMakeLists.txt index e58ba83e7..bf136a567 100644 --- a/modules/Auth/CMakeLists.txt +++ b/modules/Auth/CMakeLists.txt @@ -21,6 +21,7 @@ target_link_libraries(${MODULE_NAME} date::date date::date-tz everest::timer + everest::staging::helpers ) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 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..b6c66ff22 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,15 +224,12 @@ class AuthHandler { bool ignore_faults; ReservationHandler reservation_handler; - std::map> connectors; + std::map> evses; - std::mutex timer_mutex; std::list plug_in_queue; - std::mutex plug_in_queue_mutex; - std::mutex plug_in_mutex; std::set tokens_in_process; - std::mutex token_in_process_mutex; std::condition_variable cv; + std::mutex event_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); + 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); + /** + * @brief Check reservations: if there are as many reservations as evse's, all should be set to reserved. + * + * This will check the reservation status of the evse's and send the statusses to the evse manager. + */ + void check_evse_reserved_and_send_updates(); }; } // namespace module diff --git a/modules/Auth/include/Connector.hpp b/modules/Auth/include/Connector.hpp index 56cd47b06..f53bff281 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,44 @@ 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), + plug_in_timeout(false) { + } + + int32_t evse_id; + int32_t evse_index; + bool transaction_active; - int evse_index; - Connector connector; + // 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 plug_in_timeout; // indicates no authorization received within connection_timeout. Replug is required for this + // EVSE to get authorization and start a transaction + + 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..e35497a15 100644 --- a/modules/Auth/include/ReservationHandler.hpp +++ b/modules/Auth/include/ReservationHandler.hpp @@ -1,94 +1,355 @@ -// 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 -#include + +class kvsIntf; namespace module { +struct ReservationEvseStatus { + std::set reserved; + std::set available; +}; + class ReservationHandler { +private: // Members + /// \brief Map of EVSE's, with EVSE id as key and the EVSE struct as value. + std::map>& evses; + /// \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 event mutex, for all timer bound locks (for `reservation_id_to_reservation_timeout_timer_map`) + mutable std::recursive_mutex event_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::set last_reserved_status; - 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, 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 Cancel a reservation. + /// \param evse_id The evse id to cancel the reservation for. + /// \param execute_callback True if the `reservation_cancelled_callback` must be called. + /// \return True if the reservation could be cancelled. + /// + bool cancel_reservation(const uint32_t evse_id, const bool execute_callback); + + /// + /// \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); + + /// + /// \brief Check if the number of global reservations match the number of available evse's. + /// \return The new reservation status of the evse's. + /// + /// \note The return value has the new reserved and new available statusses (so the ones that were already reserved + /// are not added to those lists). + /// + ReservationEvseStatus check_number_global_reservations_match_number_available_evses(); + +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 Get new reserved / available status for evse's and store it. + /// \param currently_available_evses Current available evse's. + /// \param reserved_evses Current reserved evse's. + /// \return A struct with changed reservation statuses compared with the last time this function was called. + /// + /// When an evse is reserved and it was available before, it will be added to the set in the struct (return value). + /// But when an evse is reserved and last time it was already reserved, it is not added. + /// + ReservationEvseStatus + get_evse_global_reserved_status_and_set_new_status(const std::set& currently_available_evses, + const std::set& reserved_evses); + + /// + /// \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 c87a23b91..bffd1f1e6 100644 --- a/modules/Auth/lib/AuthHandler.cpp +++ b/modules/Auth/lib/AuthHandler.cpp @@ -2,7 +2,10 @@ // Copyright Pionix GmbH and Contributors to EVerest #include + #include +#include +#include namespace module { @@ -38,41 +41,53 @@ 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, 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) { + std::lock_guard lock(this->event_mutex); + 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; + } + + this->evses[evse_id] = std::make_unique(evse_id, evse_index, connectors); } -TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) { +void AuthHandler::initialize() { + std::lock_guard lock(this->event_mutex); + this->reservation_handler.load_reservations(); + check_evse_reserved_and_send_updates(); +} +TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) { + this->event_mutex.lock(); // lock mutex directly because it needs to be unlocked within handle_token TokenHandlingResult result; // 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); + EVLOG_info << "Received new token: " << everest::staging::helpers::redact(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); } else { // do nothing if token is currently processed - EVLOG_info << "Received token " << provided_token.id_token.value << " repeatedly while still processing it"; - this->token_in_process_mutex.unlock(); + EVLOG_info << "Received token " << everest::staging::helpers::redact(provided_token.id_token.value) + << " repeatedly while still processing it"; result = TokenHandlingResult::ALREADY_IN_PROCESS; } @@ -94,67 +109,77 @@ TokenHandlingResult AuthHandler::on_token(const ProvidedIdToken& provided_token) } if (result != TokenHandlingResult::ALREADY_IN_PROCESS) { - std::lock_guard lk(this->token_in_process_mutex); this->tokens_in_process.erase(provided_token.id_token.value); } - EVLOG_info << "Result for token: " << provided_token.id_token.value << ": " + EVLOG_info << "Result for token: " << everest::staging::helpers::redact(provided_token.id_token.value) << ": " << conversions::token_handling_result_to_string(result); + this->event_mutex.unlock(); return result; } 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,13 @@ 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) { + + 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 +228,16 @@ 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) { + 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 +246,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 +256,66 @@ 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 + this->event_mutex + .unlock(); // unlock to allow other threads to continue processing in case select_evse is blocking + int evse_id = this->select_evse(referenced_evses); // might block + this->event_mutex.lock(); // lock again after evse is selected + EVLOG_debug << "Selected evse#" << evse_id + << " for token: " << everest::staging::helpers::redact(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: " + << everest::staging::helpers::redact(provided_token); return TokenHandlingResult::TIMEOUT; } } @@ -294,7 +327,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 +343,60 @@ 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; // 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) { + 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 { // 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,22 @@ 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..."; + 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) { + 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 +440,48 @@ bool AuthHandler::equals_master_pass_group_id(const std::optionalmaster_pass_group_id.value(); } -int AuthHandler::get_latest_plugin(const std::vector& connectors) { - 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; +int AuthHandler::get_latest_plugin(const std::vector& evse_ids) { + for (const auto evse_id : this->plug_in_queue) { + if (std::find(evse_ids.begin(), evse_ids.end(), evse_id) != evse_ids.end()) { + return evse_id; } } return -1; } -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::unlock_plug_in_mutex(const std::vector& connectors) { - for (const auto connector_id : connectors) { - this->connectors.at(connector_id)->plug_in_mutex.unlock(); - } -} - -int AuthHandler::select_connector(const std::vector& connectors) { - - if (connectors.size() == 1) { - return connectors.at(0); +int AuthHandler::select_evse(const std::vector& selected_evses) { + std::unique_lock lk(this->event_mutex); + 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..."; - std::unique_lock lk(this->plug_in_mutex); - // blocks until respective plugin for connector occured or until timeout + // locks all referenced evses for this request. Subsequent requests referencing one or more of the locked + // evses are blocked until handle_token returns + if (this->get_latest_plugin(selected_evses) == -1) { + // no EV has been plugged in yet at the referenced evses + EVLOG_debug << "No evse in authorization queue. Waiting for a plug in..."; + // blocks until respective plugin for evse 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"; + const auto selected_evse_id = this->get_latest_plugin(selected_evses); + if (selected_evse_id != -1 and !this->evses.at(selected_evse_id)->transaction_active) { + // an EV has been plugged in yet at the referenced evses + 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 +492,218 @@ 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; + 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); - - 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]() { - EVLOG_info << "Authorization timeout for evse#" << evse_index; - this->connectors.at(connector_id)->connector.identifier.reset(); - this->withdraw_authorization_callback(evse_index); - }, - 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->evses.at(evse_id)->identifier.emplace(identifier); + + this->evses.at(evse_id)->timeout_timer.stop(); + this->evses.at(evse_id)->timeout_timer.timeout( + [this, evse_index, evse_id, provided_token]() { + std::lock_guard lk(this->event_mutex); + 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)); + 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::lock_guard lk(this->event_mutex); + 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::lock_guard lk(this->event_mutex); + 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}; } -void AuthHandler::call_reserved(const int& connector_id, const int reservation_id) { - this->reserved_callback(this->connectors.at(connector_id)->evse_index, reservation_id); +ReservationCheckStatus AuthHandler::handle_reservation_exists(std::string& id_token, const std::optional& evse_id, + std::optional& group_id_token) { + std::lock_guard lk(this->event_mutex); + // 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; +} + +bool AuthHandler::call_reserved(const int reservation_id, const std::optional& evse_id) { + const bool reserved = this->reserved_callback(evse_id, reservation_id); + if (reserved) { + this->check_evse_reserved_and_send_updates(); + } + + return reserved; } -void AuthHandler::call_reservation_cancelled(const 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) { + std::lock_guard lk(this->event_mutex); 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) { + std::lock_guard lk(this->event_mutex); 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) { + std::lock_guard lk(this->event_mutex); + // When connector id is not specified, it is assumed to be '1'. + const int32_t connector_id = event.connector_id.value_or(1); + if (evse_id < 0) { + EVLOG_error << "Handle session event: Evse id is 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; + } + + 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(); const auto event_type = event.event; + bool check_reservations = false; 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); - } - this->cv.notify_one(); + case SessionEventEnum::SessionStarted: { + this->plug_in_queue.push_back(evse_id); + this->cv.notify_all(); // 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]() { + std::lock_guard lk(this->event_mutex); + + EVLOG_info << "Plug In timeout for evse#" << evse_id << ". Replug required for this EVSE"; + this->withdraw_authorization_callback(this->evses.at(evse_id)->evse_index); + + this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; }); + this->evses.at(evse_id)->plug_in_timeout = true; + }, + 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(); + check_reservations = true; 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(); - { - std::lock_guard lk(this->plug_in_queue_mutex); - this->plug_in_queue.remove_if([connector_id](int value) { return value == connector_id; }); - } + case SessionEventEnum::SessionFinished: { + this->evses.at(evse_id)->plugged_in = false; + this->evses.at(evse_id)->plug_in_timeout = false; + this->evses.at(evse_id)->identifier.reset(); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::SESSION_FINISHED); + this->evses.at(evse_id)->timeout_timer.stop(); + this->plug_in_queue.remove_if([evse_id](int value) { return value == evse_id; }); + check_reservations = true; break; - + } case SessionEventEnum::Disabled: - this->connectors.at(connector_id)->connector.submit_event(ConnectorEvent::DISABLE); + this->submit_event_for_connector(evse_id, connector_id, ConnectorEvent::DISABLE); + check_reservations = true; 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); + check_reservations = true; 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; + case SessionEventEnum::ReservationEnd: { + if (reservation_handler.is_evse_reserved(evse_id)) { + reservation_handler.cancel_reservation(evse_id, true); + } break; + } /// explicitly fall through all the SessionEventEnum values we are not handling case SessionEventEnum::Authorized: [[fallthrough]]; @@ -631,14 +734,21 @@ void AuthHandler::handle_session_event(const int connector_id, const SessionEven case SessionEventEnum::PluginTimeout: break; } - this->connectors.at(connector_id)->event_mutex.unlock(); + + // When reservation is started or ended, check if the number of reservations match the number of evses and + // send 'reserved' notifications to the evse manager accordingly if needed. + if (check_reservations) { + check_evse_reserved_and_send_updates(); + } } void AuthHandler::set_connection_timeout(const int connection_timeout) { + std::lock_guard lk(this->event_mutex); this->connection_timeout = connection_timeout; }; void AuthHandler::set_master_pass_group_id(const std::string& master_pass_group_id) { + std::lock_guard lk(this->event_mutex); if (master_pass_group_id.empty()) { this->master_pass_group_id = std::nullopt; } else { @@ -647,6 +757,7 @@ void AuthHandler::set_master_pass_group_id(const std::string& master_pass_group_ } void AuthHandler::set_prioritize_authorization_over_stopping_transaction(bool b) { + std::lock_guard lk(this->event_mutex); this->prioritize_authorization_over_stopping_transaction = b; } @@ -669,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( @@ -684,4 +806,35 @@ 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; + } + } +} + +void AuthHandler::check_evse_reserved_and_send_updates() { + ReservationEvseStatus reservation_status = + this->reservation_handler.check_number_global_reservations_match_number_available_evses(); + for (const auto& available_evse : reservation_status.available) { + EVLOG_debug << "Evse " << available_evse << " is now available"; + this->reservation_cancelled_callback( + available_evse, -1, types::reservation::ReservationEndReason::GlobalReservationRequirementDropped, false); + } + + for (const auto& reserved_evse : reservation_status.reserved) { + EVLOG_debug << "Evse " << reserved_evse << " is now reserved"; + if (this->reserved_callback != nullptr) { + const bool reserved = this->reserved_callback(reserved_evse, -1); + if (!reserved) { + EVLOG_warning << "Could not reserve " << reserved_evse << " for non evse specific reservations"; + } + } + } +} + } // namespace module diff --git a/modules/Auth/lib/CMakeLists.txt b/modules/Auth/lib/CMakeLists.txt index fa539ddfa..916e00886 100644 --- a/modules/Auth/lib/CMakeLists.txt +++ b/modules/Auth/lib/CMakeLists.txt @@ -24,6 +24,7 @@ PRIVATE date::date date::date-tz everest::framework + everest::staging::helpers ) # needs c++ 14 diff --git a/modules/Auth/lib/Connector.cpp b/modules/Auth/lib/Connector.cpp index 63149905d..d3c841fb8 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,43 @@ std::string connector_state_to_string(const ConnectorState& state) { } } // namespace conversions + +bool EVSEContext::is_available() { + if (this->plug_in_timeout) { + return false; + } + + 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..37ac388eb 100644 --- a/modules/Auth/lib/ReservationHandler.cpp +++ b/modules/Auth/lib/ReservationHandler.cpp @@ -1,110 +1,873 @@ -// 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); -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(std::map>& evses, + const std::string& id, kvsIntf* store) : + evses(evses), 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::has_reservation_parent_id(int connector) { - std::lock_guard lk(this->reservation_mutex); - if (!this->reservations.count(connector)) { - return false; - } - return this->reservations.at(connector).parent_id_token.has_value(); +ReservationHandler::~ReservationHandler() { + work->get_io_context().stop(); + io_service.stop(); + io_service_thread.join(); } -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"; - return types::reservation::ReservationResult::Rejected; +void ReservationHandler::load_reservations() { + std::lock_guard lk(this->event_mutex); + if (this->store == nullptr) { + EVLOG_info << "Can not load reservations because the store is a nullptr."; + return; } - if (state == ConnectorState::UNAVAILABLE) { - EVLOG_debug << "Rejecting reservation because connector is unavailable"; - return types::reservation::ReservationResult::Unavailable; + 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; } - if (state == ConnectorState::FAULTED) { - EVLOG_debug << "Rejecting reservation because connector is faulted"; - return types::reservation::ReservationResult::Faulted; + 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); + } } +} +types::reservation::ReservationResult +ReservationHandler::make_reservation(const std::optional evse_id, + const types::reservation::Reservation& reservation) { + std::lock_guard lk(this->event_mutex); if (date::utc_clock::now() > Everest::Date::from_rfc3339(reservation.expiry_time)) { - EVLOG_debug << "Rejecting reservation because expiry_time is in the past"; + EVLOG_info << "Rejecting reservation because expire time is in the past."; return types::reservation::ReservationResult::Rejected; } - if (!is_reservable) { - EVLOG_debug << "Rejecting reservation because connector is not in state AVAILABLE"; - return types::reservation::ReservationResult::Occupied; + // 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 (!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; + 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 { - EVLOG_debug << "Rejecting reservation because connector is already reserved"; - return types::reservation::ReservationResult::Occupied; + 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(); } + + 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) { + std::lock_guard lk(this->event_mutex); + if (connector_state == ConnectorState::AVAILABLE) { + // Nothing to cancel. + return; + } + + 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::lock_guard lk(this->event_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) { + std::lock_guard lk(this->event_mutex); + if (this->evse_reservations.count(evse_id) > 0) { + return true; + } + + return false; } -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) { +std::pair> +ReservationHandler::cancel_reservation(const int reservation_id, const bool execute_callback, + const types::reservation::ReservationEndReason reason) { + std::lock_guard lk(this->event_mutex); + std::pair> result; + + bool reservation_cancelled = false; + + 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) { - connector = reservation.first; + evse_id = reservation.first; } } - 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); + + 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); } } - return connector; + + 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::on_reservation_used(int connector) { - if (this->cancel_reservation(this->reservations[connector].reservation_id, false)) { - EVLOG_info << "Reservation for connector#" << connector << " used and cancelled"; +bool ReservationHandler::cancel_reservation(const uint32_t evse_id, const bool execute_callback) { + auto it = this->evse_reservations.find(evse_id); + if (it != this->evse_reservations.end()) { + int reservation_id = it->second.reservation_id; + return this + ->cancel_reservation(reservation_id, execute_callback, types::reservation::ReservationEndReason::Cancelled) + .first; } else { - EVLOG_warning - << "On reservation used called when no reservation for this connector was present. This should not happen"; + EVLOG_warning << "Could not cancel reservation with evse id " << evse_id; + return false; } } void ReservationHandler::register_reservation_cancelled_callback( - const std::function& 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) { + std::lock_guard lk(this->event_mutex); + const std::pair> cancelled = + this->cancel_reservation(reservation_id, false, 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) { + std::lock_guard lk(this->event_mutex); + EVLOG_debug << "Matches reserved identifier for evse id " << (evse_id.has_value() ? evse_id.value() : 9999) + << " and id token " << everest::staging::helpers::redact(id_token) << " and parent id token " + << (parent_id_token.has_value() ? everest::staging::helpers::redact(parent_id_token.value()) : "-"); + + // 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 lk(this->event_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; +} + +ReservationEvseStatus ReservationHandler::check_number_global_reservations_match_number_available_evses() { + std::unique_lock reservation_lock(this->event_mutex); + std::set available_evses; + // Get all evse's that are not reserved or used. + for (const auto& evse : this->evses) { + if (get_evse_connector_state_reservation_result(static_cast(evse.first), this->evse_reservations) == + types::reservation::ReservationResult::Accepted && + get_connector_availability_reservation_result(static_cast(evse.first), + types::evse_manager::ConnectorTypeEnum::Unknown) == + types::reservation::ReservationResult::Accepted) { + available_evses.insert(evse.first); + } + } + if (available_evses.size() == this->global_reservations.size()) { + // There are as many evses available as 'global' reservations, so all evse's are reserved. Set all available + // evse's to reserved. + return get_evse_global_reserved_status_and_set_new_status(available_evses, available_evses); + } + + // There are not as many global reservations as available evse's, but we have to check for specific connector types + // as well. + std::set reserved_evses_with_specific_connector_type; + for (const auto& global_reservation : this->global_reservations) { + if (!is_reservation_possible(global_reservation.connector_type, this->global_reservations, + this->evse_reservations)) { + // A new reservation with this type is not possible (so also arrival of an extra car is not), so all evse's + // with this connector type should be set to reserved. + for (const auto& evse : this->evses) { + if (available_evses.find(evse.first) != available_evses.end() && + this->has_evse_connector_type( + evse.second->connectors, + global_reservation.connector_type.value_or(types::evse_manager::ConnectorTypeEnum::Unknown))) { + // This evse is available and has a specific connector type. So it should be set to unavailable. + reserved_evses_with_specific_connector_type.insert(evse.first); + } + } + } + } + + return get_evse_global_reserved_status_and_set_new_status(available_evses, + reserved_evses_with_specific_connector_type); +} + +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 { + 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; + } + + // Check if evse is available. + 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). + 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()); + } + } + + // If evse is reserved, it is not available. + if (evse_specific_reservations.count(evse_id) > 0) { + return types::reservation::ReservationResult::Occupied; + } + + 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) { + 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; + + 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); + + 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->event_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::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; + + 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) { + 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); +} + +void ReservationHandler::check_reservations_and_cancel_if_not_possible() { + + 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); + } + } + + 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); + } + } + + 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); + } +} + +ReservationEvseStatus ReservationHandler::get_evse_global_reserved_status_and_set_new_status( + const std::set& currently_available_evses, const std::set& reserved_evses) { + ReservationEvseStatus evse_status_to_send; + std::set new_reserved_evses; + + for (const auto evse_id : reserved_evses) { + if (this->last_reserved_status.find(evse_id) != this->last_reserved_status.end()) { + // Evse was already reserved, don't add it to the new status. + } else { + evse_status_to_send.reserved.insert(evse_id); + } + } + + for (const auto evse_id : currently_available_evses) { + const bool is_reserved = reserved_evses.find(evse_id) != reserved_evses.end(); + const bool was_reserved = this->last_reserved_status.find(evse_id) != this->last_reserved_status.end(); + if (not is_reserved) { + if (was_reserved) { + evse_status_to_send.available.insert(evse_id); + } + } + } + + new_reserved_evses = reserved_evses; + this->last_reserved_status = new_reserved_evses; + + return evse_status_to_send; +} + +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 { + 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; +} + +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..eedf297cd 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) @@ -19,8 +29,8 @@ target_link_libraries(${TEST_TARGET_NAME} PRIVATE ${CMAKE_DL_LIBS} everest::log everest::framework + everest::staging::helpers 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 5f2f7981e..d4d7c0502 100644 --- a/modules/Auth/tests/auth_tests.cpp +++ b/modules/Auth/tests/auth_tests.cpp @@ -13,9 +13,12 @@ using ::testing::_; using ::testing::Field; +using ::testing::Invoke; using ::testing::MockFunction; using ::testing::StrictMock; +class kvsIntf; + namespace types { namespace authorization { @@ -84,8 +87,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 +136,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 { @@ -190,6 +199,145 @@ TEST_F(AuthTest, test_two_referenced_connectors) { ASSERT_FALSE(this->auth_receiver->get_authorization(0)); } +/// \brief Test if connector that triggered a SessionStarted event receives authorization when two connectors are +/// referenced in the provided token +TEST_F(AuthTest, test_multiple_referenced_connectors) { + + this->auth_handler->init_evse(3, 2, {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2)}); + this->auth_receiver->add_evse_index(2); + + std::vector connectors1{1, 2}; + ProvidedIdToken provided_token1 = get_provided_token(VALID_TOKEN_1, connectors1); + + std::vector connectors2 = {3}; + ProvidedIdToken provided_token2 = get_provided_token(VALID_TOKEN_2, connectors2); + + std::mutex mtx; + std::condition_variable cv; + bool processing_called = false; + + // Set up expectations for mock_publish_token_validation_status_callback + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Processing)) + .WillOnce(Invoke([&]() { + std::unique_lock lock(mtx); + processing_called = true; + cv.notify_all(); + })); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Accepted)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::TimedOut)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Processing)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Accepted)); + + TokenHandlingResult result1; + std::thread t1([this, &result1, provided_token1] { result1 = this->auth_handler->on_token(provided_token1); }); + + // Wait for TokenValidationStatus::Processing to be triggered for t1 before starting t2 + { + std::unique_lock lock(mtx); + cv.wait(lock, [&]() { return processing_called; }); + } + + TokenHandlingResult result2; + std::thread t2([this, &result2, provided_token2] { result2 = this->auth_handler->on_token(provided_token2); }); + + const SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + this->auth_handler->handle_session_event(3, session_event); + + t2.join(); + + ASSERT_TRUE(result2 == TokenHandlingResult::ACCEPTED); + + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_TRUE(this->auth_receiver->get_authorization(2)); + + SessionEvent session_event2 = get_transaction_started_event(provided_token2); + this->auth_handler->handle_session_event(3, session_event2); + + t1.join(); + ASSERT_TRUE(result1 == TokenHandlingResult::TIMEOUT); +} + +/// \brief Test three authorization requests for different referenced EVSEs with only one EV plugin. Two requests should +/// timeout, one should receive authorization +TEST_F(AuthTest, test_multiple_authorization_requests) { + std::vector connectors1{1, 2}; + std::vector connectors2{3, 4}; + std::vector connectors3{5, 6}; + + this->auth_handler->init_evse(3, 2, {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2)}); + this->auth_handler->init_evse(4, 3, {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2)}); + this->auth_handler->init_evse(5, 4, {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2)}); + this->auth_handler->init_evse(6, 5, {Connector(1, types::evse_manager::ConnectorTypeEnum::sType2)}); + this->auth_receiver->add_evse_index(2); + this->auth_receiver->add_evse_index(3); + this->auth_receiver->add_evse_index(4); + this->auth_receiver->add_evse_index(5); + + ProvidedIdToken provided_token1 = get_provided_token(VALID_TOKEN_1, connectors1); + ProvidedIdToken provided_token2 = get_provided_token(VALID_TOKEN_2, connectors2); + ProvidedIdToken provided_token3 = get_provided_token(VALID_TOKEN_3, connectors3); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token1.id_token), TokenValidationStatus::TimedOut)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token2.id_token), TokenValidationStatus::TimedOut)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token3.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token3.id_token), TokenValidationStatus::Accepted)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token3.id_token), TokenValidationStatus::TimedOut)) + .Times(0); + + TokenHandlingResult result1; + TokenHandlingResult result2; + TokenHandlingResult result3; + + std::thread t1([this, provided_token1, &result1]() { result1 = this->auth_handler->on_token(provided_token1); }); + std::thread t2([this, provided_token2, &result2]() { result2 = this->auth_handler->on_token(provided_token2); }); + std::thread t3([this, provided_token3, &result3]() { result3 = this->auth_handler->on_token(provided_token3); }); + + SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + this->auth_handler->handle_session_event(6, session_event); + + t3.join(); + + SessionEvent transaction_started_event = get_transaction_started_event(provided_token3); + this->auth_handler->handle_session_event(6, transaction_started_event); + + ASSERT_TRUE(this->auth_receiver->get_authorization(5)); + ASSERT_TRUE(result3 == TokenHandlingResult::ACCEPTED); + + t1.join(); + t2.join(); + + ASSERT_TRUE(result1 == TokenHandlingResult::TIMEOUT); + ASSERT_TRUE(result2 == TokenHandlingResult::TIMEOUT); + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); + ASSERT_FALSE(this->auth_receiver->get_authorization(2)); + ASSERT_FALSE(this->auth_receiver->get_authorization(3)); + ASSERT_FALSE(this->auth_receiver->get_authorization(4)); +} + /// \brief Test if a transaction is stopped when an id_token is swiped twice TEST_F(AuthTest, test_stop_transaction) { std::vector connectors{1}; @@ -458,15 +606,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 +631,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 +811,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 +857,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 +928,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 +945,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 +988,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 +996,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 +1168,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); @@ -975,6 +1288,9 @@ TEST_F(AuthTest, test_authorization_without_transaction) { ASSERT_TRUE(result == TokenHandlingResult::ALREADY_IN_PROCESS); ASSERT_TRUE(this->auth_receiver->get_authorization(0)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::TimedOut)); + // wait for timeout std::this_thread::sleep_for(std::chrono::seconds(CONNECTION_TIMEOUT + 1)); @@ -1206,7 +1522,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, @@ -1226,4 +1541,30 @@ TEST_F(AuthTest, test_token_timed_out) { std::this_thread::sleep_for(std::chrono::seconds(CONNECTION_TIMEOUT + 1)); } +/// \brief Test that in case of a plug in timeout, no authorization is given to the EVSE afterwards +TEST_F(AuthTest, test_plug_in_time_out) { + const SessionEvent session_event = get_session_started_event(types::evse_manager::StartSessionReason::EVConnected); + this->auth_handler->handle_session_event(1, session_event); + + std::vector connectors{1}; + ProvidedIdToken provided_token = get_provided_token(VALID_TOKEN_1, connectors); + + std::this_thread::sleep_for(std::chrono::seconds(CONNECTION_TIMEOUT)); + + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Processing)); + EXPECT_CALL(mock_publish_token_validation_status_callback, + Call(Field(&ProvidedIdToken::id_token, provided_token.id_token), TokenValidationStatus::Rejected)); + + // no connector should be available since the plug-in event has timed out + TokenHandlingResult result; + std::thread t1([this, provided_token, &result]() { result = this->auth_handler->on_token(provided_token); }); + t1.join(); + + ASSERT_TRUE(result == TokenHandlingResult::NO_CONNECTOR_AVAILABLE); + + ASSERT_FALSE(this->auth_receiver->get_authorization(0)); + ASSERT_FALSE(this->auth_receiver->get_authorization(1)); +} + } // namespace module diff --git a/modules/Auth/tests/reservation_tests.cpp b/modules/Auth/tests/reservation_tests.cpp new file mode 100644 index 000000000..9dc6407d4 --- /dev/null +++ b/modules/Auth/tests/reservation_tests.cpp @@ -0,0 +1,1418 @@ +// 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; + ReservationHandler r{evses, "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(2); + 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(2); + 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(0); + + 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); +} + +TEST_F(ReservationHandlerTest, check_evses_to_reserve_scenario_1) { + 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); + + ReservationEvseStatus s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(1, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + ASSERT_EQ(s.reserved.size(), 1); + EXPECT_EQ(s.reserved.count(0), 1); +} + +TEST_F(ReservationHandlerTest, check_evses_to_reserve_scenario_2) { + 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); + + ReservationEvseStatus s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cCCS2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + ASSERT_EQ(s.reserved.size(), 2); + EXPECT_EQ(s.reserved.count(0), 1); + EXPECT_EQ(s.reserved.count(1), 1); +} + +TEST_F(ReservationHandlerTest, check_evses_to_reserve_scenario_3) { + 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); + + ReservationEvseStatus s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + ASSERT_EQ(s.reserved.size(), 1); + EXPECT_EQ(s.reserved.count(1), 1); +} + +TEST_F(ReservationHandlerTest, check_evses_to_reserve_scenario_4) { + 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.at(0)->plugged_in = true; + + ReservationEvseStatus s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + ASSERT_EQ(s.reserved.size(), 1); + EXPECT_EQ(s.reserved.count(1), 1); + + this->evses.at(0)->plugged_in = false; + + s = r.check_number_global_reservations_match_number_available_evses(); + + ASSERT_EQ(s.available.size(), 1); + EXPECT_EQ(s.available.count(1), 1); + EXPECT_TRUE(s.reserved.empty()); +} + +TEST_F(ReservationHandlerTest, check_evses_to_reserve_scenario_5) { + 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.at(1)->connectors.at(0).submit_event(ConnectorEvent::TRANSACTION_STARTED); + + ReservationEvseStatus s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + EXPECT_TRUE(s.reserved.empty()); + + EXPECT_EQ(r.make_reservation(std::nullopt, create_reservation(types::evse_manager::ConnectorTypeEnum::cType2)), + ReservationResult::Accepted); + + s = r.check_number_global_reservations_match_number_available_evses(); + + EXPECT_TRUE(s.available.empty()); + ASSERT_EQ(s.reserved.size(), 1); + EXPECT_EQ(s.reserved.count(0), 1); + + this->evses.at(1)->connectors.at(0).submit_event(ConnectorEvent::SESSION_FINISHED); + + s = r.check_number_global_reservations_match_number_available_evses(); + + ASSERT_EQ(s.available.size(), 1); + EXPECT_EQ(s.available.count(0), 1); + EXPECT_TRUE(s.reserved.empty()); +} + +} // 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/CMakeLists.txt b/modules/CMakeLists.txt index 98a06d1a3..beb2e4608 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -4,10 +4,12 @@ ev_add_module(EnergyManager) ev_add_module(EnergyNode) ev_add_module(EvManager) ev_add_module(ErrorHistory) +ev_add_module(Evse15118D20) ev_add_module(EvseManager) ev_add_module(EvseSecurity) ev_add_module(EvseSlac) ev_add_module(EvseV2G) +ev_add_module(IsoMux) ev_add_module(EvSlac) ev_add_module(GenericPowermeter) ev_add_module(JsTibber) @@ -24,11 +26,11 @@ ev_add_module(Store) ev_add_module(System) ev_add_module(YetiDriver) ev_add_module(YetiEvDriver) -ev_add_module(PowermeterBSM) ev_add_module(DummyV2G) ev_add_module(MicroMegaWattBSP) ev_add_module(DPM1000) ev_add_module(OCPPExtensionExample) +ev_add_module(DummyBankSessionTokenProvider) ev_add_module(DummyTokenValidator) ev_add_module(DummyTokenProvider) ev_add_module(DummyTokenProviderManual) diff --git a/modules/Cargo.lock b/modules/Cargo.lock index a298e2e5b..cafed7429 100644 --- a/modules/Cargo.lock +++ b/modules/Cargo.lock @@ -27,6 +27,7 @@ name = "RsIskraMeter" version = "0.1.0" dependencies = [ "anyhow", + "backon", "chrono", "everestrs", "everestrs-build", @@ -52,24 +53,24 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -91,15 +92,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "argh" @@ -120,7 +121,7 @@ dependencies = [ "argh_shared", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] @@ -134,9 +135,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -145,31 +146,31 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "async-trait" -version = "0.1.78" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "461abc97219de0eaaf81fe3ef974a540158f3d079c2ab200f891f1a2ef201e85" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "axum" @@ -216,19 +217,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "backon" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5289ec98f68f28dd809fd601059e6aa908bb8f6108620930828283d4ee23d7" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets", ] [[package]] @@ -245,23 +257,29 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] -name = "bytes" +name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.0.83" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -272,16 +290,52 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets", +] + +[[package]] +name = "clap" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", ] [[package]] @@ -295,44 +349,60 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cxx" -version = "1.0.110" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7129e341034ecb940c9072817cd9007974ea696844fc4dd582dc1653a7fbe2e8" +checksum = "05e1ec88093d2abd9cf1b09ffd979136b8e922bf31cad966a8fe0d73233112ef" dependencies = [ "cc", + "cxxbridge-cmd", "cxxbridge-flags", "cxxbridge-macro", + "foldhash", "link-cplusplus", ] +[[package]] +name = "cxxbridge-cmd" +version = "1.0.133" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c23bfff654d6227cbc83de8e059d2f8678ede5fc3a6c5a35d5c379983cc61e6" +dependencies = [ + "clap", + "codespan-reporting", + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "cxxbridge-flags" -version = "1.0.110" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06fdd177fc61050d63f67f5bd6351fac6ab5526694ea8e359cd9cd3b75857f44" +checksum = "f7c01b36e22051bc6928a78583f1621abaaf7621561c2ada1b00f7878fbe2caa" [[package]] name = "cxxbridge-macro" -version = "1.0.110" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "587663dd5fb3d10932c8aecfe7c844db1bcf0aee93eeab08fac13dc1212c2e7f" +checksum = "f6e14013136fac689345d17b9a6df55977251f11d333c0a571e8d963b55e1f95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "rustversion", + "syn 2.0.90", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "deranged" @@ -349,6 +419,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "downcast" version = "0.11.0" @@ -357,9 +438,9 @@ checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" [[package]] name = "either" -version = "1.10.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "endian-type" @@ -369,14 +450,14 @@ checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] @@ -401,7 +482,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "everestrs" version = "0.1.0" -source = "git+https://github.com/everest/everest-framework.git?rev=3e767e2a5652d3acb97d01fc88aae2f04f3f5282#3e767e2a5652d3acb97d01fc88aae2f04f3f5282" +source = "git+https://github.com/everest/everest-framework.git?tag=v0.19.1#6ff5d21b512e43397c537a8167dfec2136cb654a" dependencies = [ "argh", "cxx", @@ -409,13 +490,14 @@ dependencies = [ "log", "serde", "serde_json", + "serde_yaml", "thiserror", ] [[package]] name = "everestrs-build" version = "0.1.0" -source = "git+https://github.com/everest/everest-framework.git?rev=3e767e2a5652d3acb97d01fc88aae2f04f3f5282#3e767e2a5652d3acb97d01fc88aae2f04f3f5282" +source = "git+https://github.com/everest/everest-framework.git?tag=v0.19.1#6ff5d21b512e43397c537a8167dfec2136cb654a" dependencies = [ "anyhow", "argh", @@ -426,6 +508,12 @@ dependencies = [ "serde_yaml", ] +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "float-cmp" version = "0.9.0" @@ -441,6 +529,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -458,9 +552,9 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -473,9 +567,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -483,15 +577,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -500,38 +594,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -547,9 +641,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -558,15 +652,27 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-timers" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] [[package]] name = "h2" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", @@ -574,7 +680,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 2.1.0", + "indexmap 2.7.0", "slab", "tokio", "tokio-util", @@ -589,21 +695,21 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" [[package]] name = "hex" @@ -635,9 +741,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -653,9 +759,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -689,9 +795,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -710,6 +816,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "idna" version = "0.4.0" @@ -722,12 +946,23 @@ dependencies = [ [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -742,25 +977,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.15.2", ] [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is-terminal" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" dependencies = [ "hermit-abi", "libc", @@ -778,30 +1013,31 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "link-cplusplus" @@ -812,11 +1048,17 @@ dependencies = [ "cc", ] +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "matchit" @@ -826,9 +1068,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -838,31 +1080,31 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minijinja" -version = "1.0.10" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "208758577ef2c86cf5dd3e85730d161413ec3284e2d73b2ef65d9a24d9971bcb" +checksum = "55e877d961d4f96ce13615862322df7c0b6d169d40cab71a7ef3f9b9e594451e" dependencies = [ "serde", ] [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -891,7 +1133,7 @@ dependencies = [ "fragile", "lazy_static", "mockall_derive 0.12.1", - "predicates 3.1.0", + "predicates 3.1.2", "predicates-tree", ] @@ -916,7 +1158,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] @@ -928,7 +1170,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] @@ -960,42 +1202,32 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.32.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "percent-encoding" @@ -1005,29 +1237,29 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -1043,9 +1275,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "predicates" @@ -1063,9 +1298,9 @@ dependencies = [ [[package]] name = "predicates" -version = "3.1.0" +version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b87bfd4605926cdfefc1c3b5f8fe560e3feca9d5552cf68c466d3d8236c7e8" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" dependencies = [ "anstyle", "predicates-core", @@ -1073,15 +1308,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794032607612e7abeb4db69adb4e33590fa6cf1149e95fd7cb00e634b92f174" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" [[package]] name = "predicates-tree" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368ba315fb8c5052ab692e68a0eefec6ec57b23a36959c14496f0b0df2c0cecf" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" dependencies = [ "predicates-core", "termtree", @@ -1095,9 +1330,9 @@ checksum = "bbc83ee4a840062f368f9096d80077a9841ec117e17e7f700df81958f1451254" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -1127,9 +1362,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1176,9 +1411,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -1188,9 +1423,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1199,72 +1434,79 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_yaml" -version = "0.9.27" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cc7a1570e38322cfe4154732e5110f887ea57e22b76f4bfd32b5bdd3368666c" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.7.0", "itoa", "ryu", "serde", "unsafe-libyaml", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "slab" version = "0.4.9" @@ -1276,20 +1518,32 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "1.0.109" @@ -1303,9 +1557,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -1318,6 +1572,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1335,29 +1600,29 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "time" -version = "0.3.34" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "num-conv", @@ -1375,19 +1640,29 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -1400,19 +1675,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.36.0" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1427,20 +1701,20 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -1449,16 +1723,15 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -1515,21 +1788,21 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1538,20 +1811,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -1618,48 +1891,66 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unsafe-libyaml" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "url" -version = "2.5.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", - "idna 0.5.0", + "idna 1.0.3", "percent-encoding", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "want" version = "0.3.1" @@ -1677,34 +1968,34 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1712,70 +2003,39 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "winapi" -version = "0.3.9" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.4", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -1784,122 +2044,117 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnu" -version = "0.52.4" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_i686_msvc" -version = "0.52.4" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.4" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.4" +name = "writeable" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.4" +name = "yoke-derive" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] [[package]] name = "yore" @@ -1910,6 +2165,70 @@ dependencies = [ "thiserror", ] +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + [[package]] name = "zvt" version = "0.1.1" @@ -1949,7 +2268,7 @@ source = "git+https://github.com/Everest/zvt.git?rev=843ecf44ab592216bd4a483a075 dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.90", ] [[package]] diff --git a/modules/Cargo.toml b/modules/Cargo.toml index f3f573b8f..c333b1073 100644 --- a/modules/Cargo.toml +++ b/modules/Cargo.toml @@ -8,5 +8,5 @@ members = [ ] [workspace.dependencies] -everestrs = { git = "https://github.com/everest/everest-framework.git", rev = "3e767e2a5652d3acb97d01fc88aae2f04f3f5282" } -everestrs-build = { git = "https://github.com/everest/everest-framework.git", rev = "3e767e2a5652d3acb97d01fc88aae2f04f3f5282" } +everestrs = { git = "https://github.com/everest/everest-framework.git", tag = "v0.19.1" } +everestrs-build = { git = "https://github.com/everest/everest-framework.git", tag = "v0.19.1" } diff --git a/modules/DummyBankSessionTokenProvider/CMakeLists.txt b/modules/DummyBankSessionTokenProvider/CMakeLists.txt new file mode 100644 index 000000000..f1b5b828a --- /dev/null +++ b/modules/DummyBankSessionTokenProvider/CMakeLists.txt @@ -0,0 +1,21 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +# insert your custom targets and additional config variables here +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "main/bank_session_token_providerImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.cpp b/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.cpp new file mode 100644 index 000000000..8908b5884 --- /dev/null +++ b/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "DummyBankSessionTokenProvider.hpp" + +namespace module { + +void DummyBankSessionTokenProvider::init() { + invoke_init(*p_main); +} + +void DummyBankSessionTokenProvider::ready() { + invoke_ready(*p_main); +} + +} // namespace module diff --git a/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.hpp b/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.hpp new file mode 100644 index 000000000..972538181 --- /dev/null +++ b/modules/DummyBankSessionTokenProvider/DummyBankSessionTokenProvider.hpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef DUMMY_BANK_SESSION_TOKEN_PROVIDER_HPP +#define DUMMY_BANK_SESSION_TOKEN_PROVIDER_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf {}; + +class DummyBankSessionTokenProvider : public Everest::ModuleBase { +public: + DummyBankSessionTokenProvider() = delete; + DummyBankSessionTokenProvider(const ModuleInfo& info, std::unique_ptr p_main, + Conf& config) : + ModuleBase(info), p_main(std::move(p_main)), config(config){}; + + const std::unique_ptr p_main; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // DUMMY_BANK_SESSION_TOKEN_PROVIDER_HPP diff --git a/modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.cpp b/modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.cpp new file mode 100644 index 000000000..838b0c489 --- /dev/null +++ b/modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "bank_session_token_providerImpl.hpp" + +namespace module { +namespace main { + +void bank_session_token_providerImpl::init() { +} + +void bank_session_token_providerImpl::ready() { +} + +types::bank_transaction::BankSessionToken bank_session_token_providerImpl::handle_get_bank_session_token() { + types::bank_transaction::BankSessionToken bank_session_token; + bank_session_token.token = config.token; + return bank_session_token; +} + +} // namespace main +} // namespace module diff --git a/modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.hpp b/modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.hpp similarity index 54% rename from modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.hpp rename to modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.hpp index b560308fb..8c465a015 100644 --- a/modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.hpp +++ b/modules/DummyBankSessionTokenProvider/main/bank_session_token_providerImpl.hpp @@ -1,31 +1,34 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest -#ifndef AC_METER_SUNSPEC_AC_METER_IMPL_HPP -#define AC_METER_SUNSPEC_AC_METER_IMPL_HPP +#ifndef MAIN_BANK_SESSION_TOKEN_PROVIDER_IMPL_HPP +#define MAIN_BANK_SESSION_TOKEN_PROVIDER_IMPL_HPP // // AUTO GENERATED - MARKED REGIONS WILL BE KEPT // template version 3 // -#include +#include -#include "../PowermeterBSM.hpp" +#include "../DummyBankSessionTokenProvider.hpp" // ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 // insert your custom include headers here // ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 namespace module { -namespace ac_meter { +namespace main { -struct Conf {}; +struct Conf { + std::string token; +}; -class sunspec_ac_meterImpl : public sunspec_ac_meterImplBase { +class bank_session_token_providerImpl : public bank_session_token_providerImplBase { public: - sunspec_ac_meterImpl() = delete; - sunspec_ac_meterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - sunspec_ac_meterImplBase(ev, "ac_meter"), mod(mod), config(config){}; + bank_session_token_providerImpl() = delete; + bank_session_token_providerImpl(Everest::ModuleAdapter* ev, + const Everest::PtrContainer& mod, Conf& config) : + bank_session_token_providerImplBase(ev, "main"), mod(mod), config(config){}; // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 // insert your public definitions here @@ -33,14 +36,14 @@ class sunspec_ac_meterImpl : public sunspec_ac_meterImplBase { protected: // command handler functions (virtual) - virtual types::sunspec_ac_meter::Result handle_get_sunspec_ac_meter_value(std::string& auth_token) override; + virtual types::bank_transaction::BankSessionToken handle_get_bank_session_token() override; // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 // insert your protected definitions here // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 private: - const Everest::PtrContainer& mod; + const Everest::PtrContainer& mod; const Conf& config; virtual void init() override; @@ -55,7 +58,7 @@ class sunspec_ac_meterImpl : public sunspec_ac_meterImplBase { // insert other definitions here // ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 -} // namespace ac_meter +} // namespace main } // namespace module -#endif // AC_METER_SUNSPEC_AC_METER_IMPL_HPP +#endif // MAIN_BANK_SESSION_TOKEN_PROVIDER_IMPL_HPP diff --git a/modules/DummyBankSessionTokenProvider/manifest.yaml b/modules/DummyBankSessionTokenProvider/manifest.yaml new file mode 100644 index 000000000..d36bb826e --- /dev/null +++ b/modules/DummyBankSessionTokenProvider/manifest.yaml @@ -0,0 +1,16 @@ +description: Dummy bank session token provider +provides: + main: + description: Main implementation of bank session dummy token provider always returning one configured token + interface: bank_session_token_provider + config: + token: + description: Dummy token string to return + type: string + default: DummyBankSessionToken +requires: {} +enable_external_mqtt: false +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Kai-Uwe Hermann diff --git a/modules/DummyTokenProvider/CMakeLists.txt b/modules/DummyTokenProvider/CMakeLists.txt index 54e7f8f78..9bd167309 100644 --- a/modules/DummyTokenProvider/CMakeLists.txt +++ b/modules/DummyTokenProvider/CMakeLists.txt @@ -9,6 +9,10 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::staging::helpers +) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} diff --git a/modules/DummyTokenProvider/main/auth_token_providerImpl.cpp b/modules/DummyTokenProvider/main/auth_token_providerImpl.cpp index 5d0614ff1..9d2ed5409 100644 --- a/modules/DummyTokenProvider/main/auth_token_providerImpl.cpp +++ b/modules/DummyTokenProvider/main/auth_token_providerImpl.cpp @@ -3,6 +3,8 @@ #include "auth_token_providerImpl.hpp" +#include + namespace module { namespace main { @@ -16,9 +18,9 @@ void auth_token_providerImpl::init() { if (config.connector_id > 0) { token.connectors.emplace({config.connector_id}); } + token.parent_id_token = {config.token, types::authorization::IdTokenType::ISO14443}; - EVLOG_info << "Publishing new dummy token: " << token.id_token << " (" - << types::authorization::authorization_type_to_string(token.authorization_type) << ")"; + EVLOG_info << "Publishing new dummy token: " << everest::staging::helpers::redact(token); publish_provided_token(token); } }); 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/CMakeLists.txt b/modules/DummyTokenProviderManual/CMakeLists.txt index 54e7f8f78..9bd167309 100644 --- a/modules/DummyTokenProviderManual/CMakeLists.txt +++ b/modules/DummyTokenProviderManual/CMakeLists.txt @@ -9,6 +9,10 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::staging::helpers +) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} diff --git a/modules/DummyTokenProviderManual/main/auth_token_providerImpl.cpp b/modules/DummyTokenProviderManual/main/auth_token_providerImpl.cpp index f716a4379..c7a4592f6 100644 --- a/modules/DummyTokenProviderManual/main/auth_token_providerImpl.cpp +++ b/modules/DummyTokenProviderManual/main/auth_token_providerImpl.cpp @@ -3,13 +3,15 @@ #include "auth_token_providerImpl.hpp" +#include + namespace module { namespace main { void auth_token_providerImpl::init() { mod->mqtt.subscribe("everest_api/dummy_token_provider/cmd/provide", [this](const std::string& msg) { - json token = json::parse(msg); - EVLOG_info << "Publishing new dummy token: " << msg; + types::authorization::ProvidedIdToken token = json::parse(msg); + EVLOG_info << "Publishing new dummy token: " << everest::staging::helpers::redact(token); publish_provided_token(token); }); } 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/DummyTokenValidator/BUILD.bazel b/modules/DummyTokenValidator/BUILD.bazel index 04637151e..005aacf21 100644 --- a/modules/DummyTokenValidator/BUILD.bazel +++ b/modules/DummyTokenValidator/BUILD.bazel @@ -6,6 +6,8 @@ IMPLS = [ cc_everest_module( name = "DummyTokenValidator", - deps = [], + deps = [ + "//lib/staging/helpers", + ], impls = IMPLS, ) \ No newline at end of file diff --git a/modules/DummyTokenValidator/CMakeLists.txt b/modules/DummyTokenValidator/CMakeLists.txt index 0094c5e39..2e9eb934c 100644 --- a/modules/DummyTokenValidator/CMakeLists.txt +++ b/modules/DummyTokenValidator/CMakeLists.txt @@ -9,6 +9,10 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::staging::helpers +) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} diff --git a/modules/DummyTokenValidator/main/auth_token_validatorImpl.cpp b/modules/DummyTokenValidator/main/auth_token_validatorImpl.cpp index 79a58bb44..30d9532fa 100644 --- a/modules/DummyTokenValidator/main/auth_token_validatorImpl.cpp +++ b/modules/DummyTokenValidator/main/auth_token_validatorImpl.cpp @@ -3,6 +3,8 @@ #include "auth_token_validatorImpl.hpp" +#include + namespace module { namespace main { @@ -14,7 +16,8 @@ void auth_token_validatorImpl::ready() { types::authorization::ValidationResult auth_token_validatorImpl::handle_validate_token(types::authorization::ProvidedIdToken& provided_token) { - EVLOG_info << "Got validation request for token: " << provided_token.id_token.value; + EVLOG_info << "Got validation request for token: " + << everest::staging::helpers::redact(provided_token.id_token.value); types::authorization::ValidationResult ret; ret.authorization_status = types::authorization::string_to_authorization_status(config.validation_result); ret.reason = types::authorization::TokenValidationStatusMessage(); diff --git a/modules/DummyV2G/main/ISO15118_chargerImpl.cpp b/modules/DummyV2G/main/ISO15118_chargerImpl.cpp index d522dbdaa..64216fe05 100644 --- a/modules/DummyV2G/main/ISO15118_chargerImpl.cpp +++ b/modules/DummyV2G/main/ISO15118_chargerImpl.cpp @@ -14,11 +14,16 @@ void ISO15118_chargerImpl::ready() { void ISO15118_chargerImpl::handle_setup( types::iso15118_charger::EVSEID& evse_id, - std::vector& supported_energy_transfer_modes, + std::vector& supported_energy_transfer_modes, types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { // your code for cmd setup goes here } +void ISO15118_chargerImpl::handle_set_charging_parameters( + types::iso15118_charger::SetupPhysicalValues& physical_values) { + // your code for cmd set_charging_parameters goes here +} + void ISO15118_chargerImpl::handle_session_setup(std::vector& payment_options, bool& supported_certificate_service) { // your code for cmd session_setup goes here @@ -55,10 +60,6 @@ void ISO15118_chargerImpl::handle_stop_charging(bool& stop) { // your code for cmd stop_charging goes here } -void ISO15118_chargerImpl::handle_set_charging_parameters( - types::iso15118_charger::SetupPhysicalValues& physical_values) { -} - void ISO15118_chargerImpl::handle_update_ac_max_current(double& max_current) { // your code for cmd update_ac_max_current goes here } diff --git a/modules/DummyV2G/main/ISO15118_chargerImpl.hpp b/modules/DummyV2G/main/ISO15118_chargerImpl.hpp index df2c08c71..329c05ed1 100644 --- a/modules/DummyV2G/main/ISO15118_chargerImpl.hpp +++ b/modules/DummyV2G/main/ISO15118_chargerImpl.hpp @@ -33,9 +33,10 @@ class ISO15118_chargerImpl : public ISO15118_chargerImplBase { protected: // command handler functions (virtual) - virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, - std::vector& supported_energy_transfer_modes, - types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; + virtual void + handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) override; virtual void handle_session_setup(std::vector& payment_options, bool& supported_certificate_service) override; diff --git a/modules/EnergyManager/BrokerFastCharging.cpp b/modules/EnergyManager/BrokerFastCharging.cpp index 2a3b4b6ce..cc0dafa02 100644 --- a/modules/EnergyManager/BrokerFastCharging.cpp +++ b/modules/EnergyManager/BrokerFastCharging.cpp @@ -165,7 +165,7 @@ bool BrokerFastCharging::trade(Offer& _offer) { } } - if (number_of_phases == min_phases_import) { + if (number_of_phases == min_phases_import and time_slot_is_active) { context.ts_1ph_optimal = date::utc_clock::now(); } diff --git a/modules/EnergyManager/EnergyManager.cpp b/modules/EnergyManager/EnergyManager.cpp index dfd0c9fed..97d5396ab 100644 --- a/modules/EnergyManager/EnergyManager.cpp +++ b/modules/EnergyManager/EnergyManager.cpp @@ -185,8 +185,8 @@ std::vector EnergyManager::run_optimizer(types::e std::vector optimized_values; optimized_values.reserve(brokers.size()); - for (auto broker : brokers) { - auto local_market = broker->get_local_market(); + for (auto& broker : brokers) { + auto& local_market = broker->get_local_market(); const auto sold_energy = local_market.get_sold_energy(); if (sold_energy.size() > 0) { diff --git a/modules/EnergyManager/Market.cpp b/modules/EnergyManager/Market.cpp index 3ad756236..f32427744 100644 --- a/modules/EnergyManager/Market.cpp +++ b/modules/EnergyManager/Market.cpp @@ -132,6 +132,45 @@ void time_probe::pause() { } } +// returns the smaller of two optionals. Note that comparison operators on optionals are a little weird if not both +// sides have a value, we explicitly want: +// - If both are not set, it should return an empty optional +// - If either a or b is set but not both, return the one set. +// - If both have a value, return the smaller one. +template std::optional min_optional(std::optional a, std::optional b) { + + if (a.has_value() and b.has_value()) { + if (a < b) { + return a; + } else { + return b; + } + } + + if (a.has_value()) { + return a; + } + + return b; +} + +template std::optional max_optional(std::optional a, std::optional b) { + + if (a.has_value() and b.has_value()) { + if (a > b) { + return a; + } else { + return b; + } + } + + if (a.has_value()) { + return a; + } + + return b; +} + ScheduleReq Market::get_max_available_energy(const ScheduleReq& request) { ScheduleReq available = globals.empty_schedule_req; @@ -156,33 +195,31 @@ ScheduleReq Market::get_max_available_energy(const ScheduleReq& request) { } if (r != request.end()) { - // apply watt limit from leaf side to root side - if ((*r).limits_to_leaves.total_power_W.has_value()) { - a.limits_to_root.total_power_W = - (*r).limits_to_leaves.total_power_W.value() / (*r).conversion_efficiency.value_or(1.); - } - // do we have a lower watt limit on root side? - if ((*r).limits_to_root.total_power_W.has_value() && a.limits_to_root.total_power_W.has_value() && - a.limits_to_root.total_power_W.value() > (*r).limits_to_root.total_power_W.value()) { - a.limits_to_root.total_power_W = (*r).limits_to_root.total_power_W.value(); - } - // apply ampere limit from leaf side to root side - if ((*r).limits_to_leaves.ac_max_current_A.has_value()) { - a.limits_to_root.ac_max_current_A = - (*r).limits_to_leaves.ac_max_current_A.value() / (*r).conversion_efficiency.value_or(1.); - } - // do we have a lower ampere limit on root side? - if ((*r).limits_to_root.ac_max_current_A.has_value() and - (a.limits_to_root.ac_max_current_A > (*r).limits_to_root.ac_max_current_A.value() or - not(*r).limits_to_leaves.ac_max_current_A.has_value())) { - a.limits_to_root.ac_max_current_A = (*r).limits_to_root.ac_max_current_A.value(); + + { + auto leaves_power_W = (*r).limits_to_leaves.total_power_W; + if (leaves_power_W.has_value()) { + leaves_power_W = leaves_power_W.value() / (*r).conversion_efficiency.value_or(1.); + } + + a.limits_to_root.total_power_W = min_optional(leaves_power_W, (*r).limits_to_root.total_power_W); } + + a.limits_to_root.ac_max_current_A = + min_optional((*r).limits_to_leaves.ac_max_current_A, (*r).limits_to_root.ac_max_current_A); + + a.limits_to_root.ac_min_phase_count = + max_optional((*r).limits_to_root.ac_min_phase_count, (*r).limits_to_leaves.ac_min_phase_count); + + a.limits_to_root.ac_max_phase_count = + min_optional((*r).limits_to_root.ac_max_phase_count, (*r).limits_to_leaves.ac_max_phase_count); + + a.limits_to_root.ac_min_current_A = + max_optional((*r).limits_to_root.ac_min_current_A, (*r).limits_to_leaves.ac_min_current_A); + // all request limits have been merged on root side in available. // copy other information if any a.price_per_kwh = (*r).price_per_kwh; - a.limits_to_root.ac_min_current_A = (*r).limits_to_root.ac_min_current_A; - a.limits_to_root.ac_min_phase_count = (*r).limits_to_root.ac_min_phase_count; - a.limits_to_root.ac_max_phase_count = (*r).limits_to_root.ac_max_phase_count; a.limits_to_root.ac_number_of_active_phases = (*r).limits_to_root.ac_number_of_active_phases; } } @@ -249,10 +286,6 @@ Market::Market(types::energy::EnergyFlowRequest& _energy_flow_request, const flo } } -const std::vector& Market::children() { - return _children; -} - ScheduleRes Market::get_sold_energy() { return sold_root; } diff --git a/modules/EnergyManager/Market.hpp b/modules/EnergyManager/Market.hpp index 145929d30..0f9d9d275 100644 --- a/modules/EnergyManager/Market.hpp +++ b/modules/EnergyManager/Market.hpp @@ -68,7 +68,6 @@ class Market { ScheduleRes get_sold_energy(); Market* parent(); - const std::vector& children(); float nominal_ac_voltage(); @@ -77,7 +76,7 @@ class Market { private: Market* _parent; - std::vector _children; + std::list _children; float _nominal_ac_voltage; // main data structures diff --git a/modules/Evse15118D20/CMakeLists.txt b/modules/Evse15118D20/CMakeLists.txt new file mode 100644 index 000000000..e0a6fc3a7 --- /dev/null +++ b/modules/Evse15118D20/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +target_sources(${MODULE_NAME} + PRIVATE + charger/session_logger.cpp +) + +target_link_libraries(${MODULE_NAME} + PRIVATE + iso15118::iso15118 +) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "charger/ISO15118_chargerImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/Evse15118D20/Evse15118D20.cpp b/modules/Evse15118D20/Evse15118D20.cpp new file mode 100644 index 000000000..c139fdbc5 --- /dev/null +++ b/modules/Evse15118D20/Evse15118D20.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "Evse15118D20.hpp" + +namespace module { + +void Evse15118D20::init() { + invoke_init(*p_charger); +} + +void Evse15118D20::ready() { + invoke_ready(*p_charger); +} + +} // namespace module diff --git a/modules/Evse15118D20/Evse15118D20.hpp b/modules/Evse15118D20/Evse15118D20.hpp new file mode 100644 index 000000000..7d9bd1360 --- /dev/null +++ b/modules/Evse15118D20/Evse15118D20.hpp @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef EVSE15118D20_HPP +#define EVSE15118D20_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + std::string device; + std::string certificate_path; + std::string logging_path; + std::string tls_negotiation_strategy; + std::string private_key_password; + bool enable_ssl_logging; + bool enable_tls_key_logging; + bool enable_sdp_server; + bool supported_dynamic_mode; + bool supported_mobility_needs_mode_provided_by_secc; + bool supported_scheduled_mode; +}; + +class Evse15118D20 : public Everest::ModuleBase { +public: + Evse15118D20() = delete; + Evse15118D20(const ModuleInfo& info, std::unique_ptr p_charger, Conf& config) : + ModuleBase(info), p_charger(std::move(p_charger)), config(config){}; + + const std::unique_ptr p_charger; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // EVSE15118D20_HPP diff --git a/modules/Evse15118D20/charger/ISO15118_chargerImpl.cpp b/modules/Evse15118D20/charger/ISO15118_chargerImpl.cpp new file mode 100644 index 000000000..dbdfcee84 --- /dev/null +++ b/modules/Evse15118D20/charger/ISO15118_chargerImpl.cpp @@ -0,0 +1,506 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "ISO15118_chargerImpl.hpp" + +#include "session_logger.hpp" +#include "utils.hpp" + +#include +#include + +namespace module { +namespace charger { + +static constexpr auto WAIT_FOR_SETUP_DONE_MS{std::chrono::milliseconds(200)}; + +std::mutex GEL; // Global EVerest Lock + +namespace dt = iso15118::message_20::datatypes; + +namespace { + +std::filesystem::path construct_cert_path(const std::filesystem::path& initial_path, const std::string& config_path) { + if (config_path.empty()) { + return initial_path; + } + + if (config_path.front() == '/') { + return config_path; + } else { + return initial_path / config_path; + } +} + +iso15118::config::TlsNegotiationStrategy convert_tls_negotiation_strategy(const std::string& strategy) { + using Strategy = iso15118::config::TlsNegotiationStrategy; + if (strategy == "ACCEPT_CLIENT_OFFER") { + return Strategy::ACCEPT_CLIENT_OFFER; + } else if (strategy == "ENFORCE_TLS") { + return Strategy::ENFORCE_TLS; + } else if (strategy == "ENFORCE_NO_TLS") { + return Strategy::ENFORCE_NO_TLS; + } else { + EVLOG_AND_THROW(Everest::EverestConfigError("Invalid choice for tls_negotiation_strategy: " + strategy)); + // better safe than sorry + } +} + +template std::optional convert_from_optional(const std::optional& in) { + return (in) ? std::make_optional(static_cast(*in)) : std::nullopt; +} + +template <> std::optional convert_from_optional(const std::optional& in) { + return (in) ? std::make_optional(dt::from_RationalNumber(*in)) : std::nullopt; +} + +types::iso15118_charger::DisplayParameters convert_display_parameters(const dt::DisplayParameters& in) { + return {in.present_soc, + in.min_soc, + in.target_soc, + in.max_soc, + in.remaining_time_to_min_soc, + in.remaining_time_to_target_soc, + in.remaining_time_to_max_soc, + in.charging_complete, + convert_from_optional(in.battery_energy_capacity), + in.inlet_hot}; +} + +types::iso15118_charger::DcChargeDynamicModeValues convert_dynamic_values(const dt::Dynamic_DC_CLReqControlMode& in) { + return {dt::from_RationalNumber(in.target_energy_request), + dt::from_RationalNumber(in.max_energy_request), + dt::from_RationalNumber(in.min_energy_request), + dt::from_RationalNumber(in.max_charge_power), + dt::from_RationalNumber(in.min_charge_power), + dt::from_RationalNumber(in.max_charge_current), + dt::from_RationalNumber(in.max_voltage), + dt::from_RationalNumber(in.min_voltage), + convert_from_optional(in.departure_time), + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt, + std::nullopt}; +} + +types::iso15118_charger::DcChargeDynamicModeValues +convert_dynamic_values(const iso15118::message_20::datatypes::BPT_Dynamic_DC_CLReqControlMode& in) { + return {dt::from_RationalNumber(in.target_energy_request), + dt::from_RationalNumber(in.max_energy_request), + dt::from_RationalNumber(in.min_energy_request), + dt::from_RationalNumber(in.max_charge_power), + dt::from_RationalNumber(in.min_charge_power), + dt::from_RationalNumber(in.max_charge_current), + dt::from_RationalNumber(in.max_voltage), + dt::from_RationalNumber(in.min_voltage), + convert_from_optional(in.departure_time), + dt::from_RationalNumber(in.max_discharge_power), + dt::from_RationalNumber(in.min_discharge_power), + dt::from_RationalNumber(in.max_discharge_current), + convert_from_optional(in.max_v2x_energy_request), + convert_from_optional(in.min_v2x_energy_request)}; +} + +auto fill_mobility_needs_modes_from_config(const module::Conf& module_config) { + + std::vector mobility_needs_modes{}; + + if (module_config.supported_dynamic_mode) { + mobility_needs_modes.push_back({dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedByEvcc}); + if (module_config.supported_mobility_needs_mode_provided_by_secc) { + mobility_needs_modes.push_back({dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedBySecc}); + } + } + + if (module_config.supported_scheduled_mode) { + mobility_needs_modes.push_back({dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}); + } + + if (mobility_needs_modes.empty()) { + EVLOG_warning << "Control mobility modes are empty! Setting dynamic mode as default!"; + mobility_needs_modes.push_back({dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedByEvcc}); + } + + return mobility_needs_modes; +} + +} // namespace + +void ISO15118_chargerImpl::init() { + + // setup logging routine + iso15118::io::set_logging_callback([](const iso15118::LogLevel& level, const std::string& msg) { + switch (level) { + case iso15118::LogLevel::Error: + EVLOG_error << msg; + break; + case iso15118::LogLevel::Warning: + EVLOG_warning << msg; + break; + case iso15118::LogLevel::Info: + EVLOG_info << msg; + break; + case iso15118::LogLevel::Debug: + EVLOG_debug << msg; + break; + case iso15118::LogLevel::Trace: + EVLOG_verbose << msg; + break; + default: + EVLOG_critical << "(Loglevel not defined) - " << msg; + break; + } + }); +} + +void ISO15118_chargerImpl::ready() { + + while (true) { + if (setup_steps_done.all()) { + break; + } + std::this_thread::sleep_for(WAIT_FOR_SETUP_DONE_MS); + } + + const auto session_logger = std::make_unique(mod->config.logging_path); + + const auto default_cert_path = mod->info.paths.etc / "certs"; + const auto cert_path = construct_cert_path(default_cert_path, mod->config.certificate_path); + const iso15118::TbdConfig tbd_config = { + { + iso15118::config::CertificateBackend::EVEREST_LAYOUT, + cert_path.string(), + mod->config.private_key_password, + mod->config.enable_ssl_logging, + mod->config.enable_tls_key_logging, + }, + mod->config.device, + convert_tls_negotiation_strategy(mod->config.tls_negotiation_strategy), + mod->config.enable_sdp_server, + }; + const auto callbacks = create_callbacks(); + + setup_config.control_mobility_modes = fill_mobility_needs_modes_from_config(mod->config); + + controller = std::make_unique(tbd_config, callbacks, setup_config); + + try { + controller->loop(); + } catch (const std::exception& e) { + EVLOG_error << e.what(); + } +} + +iso15118::session::feedback::Callbacks ISO15118_chargerImpl::create_callbacks() { + + using ScheduleControlMode = dt::Scheduled_DC_CLReqControlMode; + using BPT_ScheduleReqControlMode = dt::BPT_Scheduled_DC_CLReqControlMode; + using DynamicReqControlMode = dt::Dynamic_DC_CLReqControlMode; + using BPT_DynamicReqControlMode = dt::BPT_Dynamic_DC_CLReqControlMode; + + namespace feedback = iso15118::session::feedback; + + feedback::Callbacks callbacks; + + callbacks.dc_pre_charge_target_voltage = [this](float target_voltage) { + publish_dc_ev_target_voltage_current({target_voltage, 0}); + }; + + callbacks.dc_charge_loop_req = [this](const feedback::DcChargeLoopReq& dc_charge_loop_req) { + if (const auto* dc_control_mode = std::get_if(&dc_charge_loop_req)) { + if (const auto* scheduled_mode = std::get_if(dc_control_mode)) { + const auto target_voltage = dt::from_RationalNumber(scheduled_mode->target_voltage); + const auto target_current = dt::from_RationalNumber(scheduled_mode->target_current); + + publish_dc_ev_target_voltage_current({target_voltage, target_current}); + + if (scheduled_mode->max_charge_current and scheduled_mode->max_voltage and + scheduled_mode->max_charge_power) { + const auto max_current = dt::from_RationalNumber(scheduled_mode->max_charge_current.value()); + const auto max_voltage = dt::from_RationalNumber(scheduled_mode->max_voltage.value()); + const auto max_power = dt::from_RationalNumber(scheduled_mode->max_charge_power.value()); + publish_dc_ev_maximum_limits({max_current, max_power, max_voltage}); + } + + } else if (const auto* bpt_scheduled_mode = std::get_if(dc_control_mode)) { + const auto target_voltage = dt::from_RationalNumber(bpt_scheduled_mode->target_voltage); + const auto target_current = dt::from_RationalNumber(bpt_scheduled_mode->target_current); + publish_dc_ev_target_voltage_current({target_voltage, target_current}); + + if (bpt_scheduled_mode->max_charge_current and bpt_scheduled_mode->max_voltage and + bpt_scheduled_mode->max_charge_power) { + const auto max_current = dt::from_RationalNumber(bpt_scheduled_mode->max_charge_current.value()); + const auto max_voltage = dt::from_RationalNumber(bpt_scheduled_mode->max_voltage.value()); + const auto max_power = dt::from_RationalNumber(bpt_scheduled_mode->max_charge_power.value()); + publish_dc_ev_maximum_limits({max_current, max_power, max_voltage}); + } + + // publish_dc_ev_maximum_limits({max_limits.current, max_limits.power, max_limits.voltage}); + } else if (const auto* dynamic_mode = std::get_if(dc_control_mode)) { + publish_d20_dc_dynamic_charge_mode(convert_dynamic_values(*dynamic_mode)); + } else if (const auto* bpt_dynamic_mode = std::get_if(dc_control_mode)) { + publish_d20_dc_dynamic_charge_mode(convert_dynamic_values(*bpt_dynamic_mode)); + } + } else if (const auto* display_parameters = std::get_if(&dc_charge_loop_req)) { + publish_display_parameters(convert_display_parameters(*display_parameters)); + } else if (const auto* present_voltage = std::get_if(&dc_charge_loop_req)) { + publish_dc_ev_present_voltage(dt::from_RationalNumber(*present_voltage)); + } else if (const auto* meter_info_requested = std::get_if(&dc_charge_loop_req)) { + if (*meter_info_requested) { + EVLOG_info << "Meter info is requested from EV"; + publish_meter_info_requested(nullptr); + } + } + }; + + callbacks.dc_max_limits = [this](const feedback::DcMaximumLimits& max_limits) { + publish_dc_ev_maximum_limits({max_limits.current, max_limits.power, max_limits.voltage}); + }; + + callbacks.signal = [this](feedback::Signal signal) { + using Signal = feedback::Signal; + switch (signal) { + case Signal::CHARGE_LOOP_STARTED: + publish_current_demand_started(nullptr); + break; + case Signal::SETUP_FINISHED: + publish_v2g_setup_finished(nullptr); + break; + case Signal::START_CABLE_CHECK: + publish_start_cable_check(nullptr); + break; + case Signal::REQUIRE_AUTH_EIM: + publish_require_auth_eim(nullptr); + break; + case Signal::CHARGE_LOOP_FINISHED: + publish_current_demand_finished(nullptr); + break; + case Signal::DC_OPEN_CONTACTOR: + publish_dc_open_contactor(nullptr); + break; + case Signal::DLINK_TERMINATE: + publish_dlink_terminate(nullptr); + break; + case Signal::DLINK_PAUSE: + publish_dlink_pause(nullptr); + break; + case Signal::DLINK_ERROR: + publish_dlink_error(nullptr); + break; + } + }; + + callbacks.v2g_message = [this](iso15118::message_20::Type id) { + const auto v2g_message_id = convert_v2g_message_type(id); + publish_v2g_messages({v2g_message_id}); + }; + + callbacks.evccid = [this](const std::string& evccid) { publish_evcc_id(evccid); }; + + callbacks.selected_protocol = [this](const std::string& protocol) { publish_selected_protocol(protocol); }; + + return callbacks; +} + +void ISO15118_chargerImpl::handle_setup( + types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { + + std::scoped_lock lock(GEL); + setup_config.evse_id = evse_id.evse_id; // TODO(SL): Check format for d20 + + std::vector services; + + for (const auto& mode : supported_energy_transfer_modes) { + if (mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::AC_single_phase_core || + mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::AC_three_phase_core) { + if (mode.bidirectional) { + services.push_back(dt::ServiceCategory::AC_BPT); + } else { + services.push_back(dt::ServiceCategory::AC); + } + } else if (mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::DC_core || + mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::DC_extended || + mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::DC_combo_core || + mode.energy_transfer_mode == types::iso15118_charger::EnergyTransferMode::DC_unique) { + if (mode.bidirectional) { + services.push_back(dt::ServiceCategory::DC_BPT); + } else { + services.push_back(dt::ServiceCategory::DC); + } + } + } + + setup_config.supported_energy_services = services; + + setup_steps_done.set(to_underlying_value(SetupStep::ENERGY_SERVICE)); +} + +void ISO15118_chargerImpl::handle_set_charging_parameters( + types::iso15118_charger::SetupPhysicalValues& physical_values) { + // your code for cmd set_charging_parameters goes here +} + +void ISO15118_chargerImpl::handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) { + std::scoped_lock lock(GEL); + + std::vector auth_services; + + for (auto& option : payment_options) { + if (option == types::iso15118_charger::PaymentOption::ExternalPayment) { + auth_services.push_back(dt::Authorization::EIM); + } else if (option == types::iso15118_charger::PaymentOption::Contract) { + // auth_services.push_back(iso15118::message_20::Authorization::PnC); + EVLOG_warning << "Currently Plug&Charge is not supported and ignored"; + } + } + + setup_config.authorization_services = auth_services; + setup_config.enable_certificate_install_service = supported_certificate_service; + + setup_steps_done.set(to_underlying_value(SetupStep::AUTH_SETUP)); +} + +void ISO15118_chargerImpl::handle_certificate_response( + types::iso15118_charger::ResponseExiStreamStatus& exi_stream_status) { + // your code for cmd certificate_response goes here +} + +void ISO15118_chargerImpl::handle_authorization_response( + types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) { + + std::scoped_lock lock(GEL); + // Todo(sl): Currently PnC is not supported + bool authorized = false; + + if (authorization_status == types::authorization::AuthorizationStatus::Accepted) { + authorized = true; + } + + if (controller) { + controller->send_control_event(iso15118::d20::AuthorizationResponse{authorized}); + } +} + +void ISO15118_chargerImpl::handle_ac_contactor_closed(bool& status) { + // your code for cmd ac_contactor_closed goes here +} + +void ISO15118_chargerImpl::handle_dlink_ready(bool& value) { + // your code for cmd dlink_ready goes here +} + +void ISO15118_chargerImpl::handle_cable_check_finished(bool& status) { + + std::scoped_lock lock(GEL); + if (controller) { + controller->send_control_event(iso15118::d20::CableCheckFinished{status}); + } +} + +void ISO15118_chargerImpl::handle_receipt_is_required(bool& receipt_required) { + // your code for cmd receipt_is_required goes here +} + +void ISO15118_chargerImpl::handle_stop_charging(bool& stop) { + + std::scoped_lock lock(GEL); + if (controller) { + controller->send_control_event(iso15118::d20::StopCharging{stop}); + } +} + +void ISO15118_chargerImpl::handle_update_ac_max_current(double& max_current) { + // your code for cmd update_ac_max_current goes here +} + +void ISO15118_chargerImpl::handle_update_dc_maximum_limits( + types::iso15118_charger::DcEvseMaximumLimits& maximum_limits) { + + std::scoped_lock lock(GEL); + setup_config.dc_limits.charge_limits.current.max = dt::from_float(maximum_limits.evse_maximum_current_limit); + setup_config.dc_limits.charge_limits.power.max = dt::from_float(maximum_limits.evse_maximum_power_limit); + setup_config.dc_limits.voltage.max = dt::from_float(maximum_limits.evse_maximum_voltage_limit); + + if (maximum_limits.evse_maximum_discharge_current_limit.has_value() or + maximum_limits.evse_maximum_discharge_power_limit.has_value()) { + auto& discharge_limits = setup_config.dc_limits.discharge_limits.emplace(); + + if (maximum_limits.evse_maximum_discharge_current_limit.has_value()) { + discharge_limits.current.max = dt::from_float(*maximum_limits.evse_maximum_discharge_current_limit); + } + + if (maximum_limits.evse_maximum_discharge_power_limit.has_value()) { + discharge_limits.power.max = dt::from_float(*maximum_limits.evse_maximum_discharge_power_limit); + } + } + + if (controller) { + controller->update_dc_limits(setup_config.dc_limits); + } + + setup_steps_done.set(to_underlying_value(SetupStep::MAX_LIMITS)); +} + +void ISO15118_chargerImpl::handle_update_dc_minimum_limits( + types::iso15118_charger::DcEvseMinimumLimits& minimum_limits) { + + std::scoped_lock lock(GEL); + setup_config.dc_limits.charge_limits.current.min = dt::from_float(minimum_limits.evse_minimum_current_limit); + + setup_config.dc_limits.charge_limits.power.min = dt::from_float(minimum_limits.evse_minimum_power_limit); + setup_config.dc_limits.voltage.min = dt::from_float(minimum_limits.evse_minimum_voltage_limit); + + if (minimum_limits.evse_minimum_discharge_current_limit.has_value() or + minimum_limits.evse_minimum_discharge_power_limit.has_value()) { + auto& discharge_limits = setup_config.dc_limits.discharge_limits.emplace(); + + if (minimum_limits.evse_minimum_discharge_current_limit.has_value()) { + discharge_limits.current.min = dt::from_float(*minimum_limits.evse_minimum_discharge_current_limit); + } + + if (minimum_limits.evse_minimum_discharge_power_limit.has_value()) { + discharge_limits.power.min = dt::from_float(*minimum_limits.evse_minimum_discharge_power_limit); + } + } + + if (controller) { + controller->update_dc_limits(setup_config.dc_limits); + } + + setup_steps_done.set(to_underlying_value(SetupStep::MIN_LIMITS)); +} + +void ISO15118_chargerImpl::handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) { + // your code for cmd update_isolation_status goes here +} + +void ISO15118_chargerImpl::handle_update_dc_present_values( + types::iso15118_charger::DcEvsePresentVoltageCurrent& present_voltage_current) { + + float voltage = present_voltage_current.evse_present_voltage; + float current = present_voltage_current.evse_present_current.value_or(0); + + std::scoped_lock lock(GEL); + if (controller) { + controller->send_control_event(iso15118::d20::PresentVoltageCurrent{voltage, current}); + } +} + +void ISO15118_chargerImpl::handle_update_meter_info(types::powermeter::Powermeter& powermeter) { + // your code for cmd update_meter_info goes here +} + +void ISO15118_chargerImpl::handle_send_error(types::iso15118_charger::EvseError& error) { + // your code for cmd send_error goes here +} + +void ISO15118_chargerImpl::handle_reset_error() { + // your code for cmd reset_error goes here +} + +} // namespace charger +} // namespace module diff --git a/modules/Evse15118D20/charger/ISO15118_chargerImpl.hpp b/modules/Evse15118D20/charger/ISO15118_chargerImpl.hpp new file mode 100644 index 000000000..34ca91853 --- /dev/null +++ b/modules/Evse15118D20/charger/ISO15118_chargerImpl.hpp @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef CHARGER_ISO15118_CHARGER_IMPL_HPP +#define CHARGER_ISO15118_CHARGER_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../Evse15118D20.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +#include + +#include "utils.hpp" + +#include +#include +#include +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace charger { + +struct Conf {}; + +class ISO15118_chargerImpl : public ISO15118_chargerImplBase { +public: + ISO15118_chargerImpl() = delete; + ISO15118_chargerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + ISO15118_chargerImplBase(ev, "charger"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void + handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; + virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) override; + virtual void handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) override; + virtual void + handle_certificate_response(types::iso15118_charger::ResponseExiStreamStatus& exi_stream_status) override; + virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) override; + virtual void handle_ac_contactor_closed(bool& status) override; + virtual void handle_dlink_ready(bool& value) override; + virtual void handle_cable_check_finished(bool& status) override; + virtual void handle_receipt_is_required(bool& receipt_required) override; + virtual void handle_stop_charging(bool& stop) override; + virtual void handle_update_ac_max_current(double& max_current) override; + virtual void handle_update_dc_maximum_limits(types::iso15118_charger::DcEvseMaximumLimits& maximum_limits) override; + virtual void handle_update_dc_minimum_limits(types::iso15118_charger::DcEvseMinimumLimits& minimum_limits) override; + virtual void handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) override; + virtual void handle_update_dc_present_values( + types::iso15118_charger::DcEvsePresentVoltageCurrent& present_voltage_current) override; + virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) override; + virtual void handle_send_error(types::iso15118_charger::EvseError& error) override; + virtual void handle_reset_error() override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + iso15118::session::feedback::Callbacks create_callbacks(); + + std::unique_ptr controller; + + iso15118::d20::EvseSetupConfig setup_config; + std::bitset setup_steps_done{0}; + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace charger +} // namespace module + +#endif // CHARGER_ISO15118_CHARGER_IMPL_HPP diff --git a/modules/Evse15118D20/charger/session_logger.cpp b/modules/Evse15118D20/charger/session_logger.cpp new file mode 100644 index 000000000..5f7860572 --- /dev/null +++ b/modules/Evse15118D20/charger/session_logger.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "session_logger.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +using LogEvent = iso15118::session::logging::Event; + +std::string get_filename_for_current_time() { + const auto now = std::chrono::system_clock::now(); + const auto now_t = std::chrono::system_clock::to_time_t(now); + + std::tm now_tm; + gmtime_r(&now_t, &now_tm); + + char buffer[64]; + strftime(buffer, sizeof(buffer), "%y%m%d_%H-%M-%S.yaml", &now_tm); + return buffer; +} + +// static auto timepoint_to_string(const iso15118::session::logging::TimePoint& timepoint) { +// using namespace date; +// return static_cast(timepoint); +// } + +std::ostream& operator<<(std::ostream& os, const iso15118::session::logging::ExiMessageDirection& direction) { + using Direction = iso15118::session::logging::ExiMessageDirection; + switch (direction) { + case Direction::FROM_EV: + return os << "FROM_EV"; + case Direction::TO_EV: + return os << "TO_EV"; + } + + return os; +} + +class SessionLog { +public: + SessionLog(const std::string& file_name) : file(file_name.c_str(), std::ios::out) { + if (not file.good()) { + throw std::runtime_error("Failed to open file " + file_name + " for writing iso15118 session log"); + } + + EVLOG_info << "Created logfile at: " << file_name; + } + void operator()(const iso15118::session::logging::SimpleEvent& event) { + file << "- type: INFO\n"; + add_timestamp(event.time_point); + file << " info: \"" << event.info << "\"\n"; + } + + void operator()(const iso15118::session::logging::ExiMessageEvent& event) { + file << "- type: EXI\n"; + add_timestamp(event.time_point); + file << " direction: " << event.direction << "\n"; + file << " sdp_payload_type: " << event.payload_type << "\n"; + add_hex_encoded_data(event.data, event.len); + } + + void flush() { + file.flush(); + } + +private: + std::fstream file; + + void add_timestamp(const iso15118::session::logging::TimePoint& timestamp) { + if (not timestamp_initialized) { + last_timestamp = timestamp; + timestamp_initialized = true; + } + + const auto offset_ms = std::chrono::duration_cast(timestamp - last_timestamp); + file << " timestamp_offset: " << offset_ms.count() << "\n"; + + const auto dp = date::floor(timestamp); + const auto time = date::make_time(timestamp - dp); + const auto milliseconds = std::chrono::duration_cast(time.subseconds()); + file << " timestamp: \""; + file << std::setfill('0') << std::setw(2) << time.hours().count() << ":"; + file << std::setfill('0') << std::setw(2) << time.minutes().count() << ":"; + file << std::setfill('0') << std::setw(2) << time.seconds().count() << "."; + file << std::setfill('0') << std::setw(4) << milliseconds.count(); + file << "\"\n"; + + last_timestamp = timestamp; + } + + void add_hex_encoded_data(const uint8_t* data, size_t len) { + file << " data: \""; + + const auto flags = file.flags(); + + file << std::hex; + + for (int i = 0; i < len; ++i) { + file << std::setfill('0') << std::setw(2) << static_cast(data[i]); + } + + file.flags(flags); + file << "\"\n"; + } + + iso15118::session::logging::TimePoint last_timestamp; + bool timestamp_initialized{false}; +}; + +SessionLogger::SessionLogger(std::filesystem::path output_dir_) : output_dir(std::filesystem::absolute(output_dir_)) { + // FIXME (aw): this is quite brute force ... + if (not std::filesystem::exists(output_dir)) { + std::filesystem::create_directory(output_dir); + } + + iso15118::session::logging::set_session_log_callback([this](std::uintptr_t id, const LogEvent& event) { + auto log_it = logs.find(id); + if (log_it == logs.end()) { + const auto log_file_name = output_dir / get_filename_for_current_time(); + const auto emplaced = logs.emplace(id, std::make_unique(log_file_name.string())); + + log_it = emplaced.first; + } + + auto& log = *log_it->second; + + std::visit(log, event); + log.flush(); + }); +} + +SessionLogger::~SessionLogger() = default; diff --git a/modules/Evse15118D20/charger/session_logger.hpp b/modules/Evse15118D20/charger/session_logger.hpp new file mode 100644 index 000000000..6bdc5982a --- /dev/null +++ b/modules/Evse15118D20/charger/session_logger.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include + +// forward declare +class SessionLog; + +class SessionLogger { +public: + SessionLogger(std::filesystem::path output_dir); + ~SessionLogger(); + +private: + std::filesystem::path output_dir; + std::map> logs; +}; diff --git a/modules/Evse15118D20/charger/utils.hpp b/modules/Evse15118D20/charger/utils.hpp new file mode 100644 index 000000000..b2155c3f5 --- /dev/null +++ b/modules/Evse15118D20/charger/utils.hpp @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include + +static constexpr auto NUMBER_OF_SETUP_STEPS = 4; + +enum class SetupStep { + ENERGY_SERVICE, + AUTH_SETUP, + MAX_LIMITS, + MIN_LIMITS, +}; + +template constexpr auto to_underlying_value(T t) { + return static_cast>(t); +} + +static_assert(NUMBER_OF_SETUP_STEPS == to_underlying_value(SetupStep::MIN_LIMITS) + 1, + "NUMBER_OF_SETUP_STEPS should be in sync with the SetupStep enum definition"); + +constexpr types::iso15118_charger::V2gMessageId convert_v2g_message_type(iso15118::message_20::Type type) { + + using Type = iso15118::message_20::Type; + using Id = types::iso15118_charger::V2gMessageId; + + switch (type) { + case Type::None: + return Id::UnknownMessage; + case Type::SupportedAppProtocolReq: + return Id::SupportedAppProtocolReq; + case Type::SupportedAppProtocolRes: + return Id::SupportedAppProtocolRes; + case Type::SessionSetupReq: + return Id::SessionSetupReq; + case Type::SessionSetupRes: + return Id::SessionSetupRes; + case Type::AuthorizationSetupReq: + return Id::AuthorizationSetupReq; + case Type::AuthorizationSetupRes: + return Id::AuthorizationSetupRes; + case Type::AuthorizationReq: + return Id::AuthorizationReq; + case Type::AuthorizationRes: + return Id::AuthorizationRes; + case Type::ServiceDiscoveryReq: + return Id::ServiceDiscoveryReq; + case Type::ServiceDiscoveryRes: + return Id::ServiceDiscoveryRes; + case Type::ServiceDetailReq: + return Id::ServiceDetailReq; + case Type::ServiceDetailRes: + return Id::ServiceDetailRes; + case Type::ServiceSelectionReq: + return Id::ServiceSelectionReq; + case Type::ServiceSelectionRes: + return Id::ServiceSelectionRes; + case Type::DC_ChargeParameterDiscoveryReq: + return Id::DcChargeParameterDiscoveryReq; + case Type::DC_ChargeParameterDiscoveryRes: + return Id::DcChargeParameterDiscoveryRes; + case Type::ScheduleExchangeReq: + return Id::ScheduleExchangeReq; + case Type::ScheduleExchangeRes: + return Id::ScheduleExchangeRes; + case Type::DC_CableCheckReq: + return Id::DcCableCheckReq; + case Type::DC_CableCheckRes: + return Id::DcCableCheckRes; + case Type::DC_PreChargeReq: + return Id::DcPreChargeReq; + case Type::DC_PreChargeRes: + return Id::DcPreChargeRes; + case Type::PowerDeliveryReq: + return Id::PowerDeliveryReq; + case Type::PowerDeliveryRes: + return Id::PowerDeliveryRes; + case Type::DC_ChargeLoopReq: + return Id::DcChargeLoopReq; + case Type::DC_ChargeLoopRes: + return Id::DcChargeLoopRes; + case Type::DC_WeldingDetectionReq: + return Id::DcWeldingDetectionReq; + case Type::DC_WeldingDetectionRes: + return Id::DcWeldingDetectionRes; + case Type::SessionStopReq: + return Id::SessionStopReq; + case Type::SessionStopRes: + return Id::SessionStopRes; + case Type::AC_ChargeParameterDiscoveryReq: + return Id::AcChargeParameterDiscoveryReq; + case Type::AC_ChargeParameterDiscoveryRes: + return Id::AcChargeParameterDiscoveryRes; + case Type::AC_ChargeLoopReq: + return Id::AcChargeLoopReq; + case Type::AC_ChargeLoopRes: + return Id::AcChargeLoopRes; + } + + return Id::UnknownMessage; +} diff --git a/modules/Evse15118D20/manifest.yaml b/modules/Evse15118D20/manifest.yaml new file mode 100644 index 000000000..18834a3e2 --- /dev/null +++ b/modules/Evse15118D20/manifest.yaml @@ -0,0 +1,70 @@ +description: >- + This module is a draft implementation of iso15118-20 for the EVSE side +config: + device: + description: >- + Ethernet device used for HLC. Any local interface that has an ipv6 + link-local and a MAC addr will work + type: string + default: eth0 + certificate_path: + description: Path to certificate directories + type: string + default: "" + logging_path: + description: Path to logging directory (will be created if non existent) + type: string + default: "." + tls_negotiation_strategy: + description: Select strategy on how to negotiate connection encryption + type: string + enum: + - ACCEPT_CLIENT_OFFER + - ENFORCE_TLS + - ENFORCE_NO_TLS + default: ACCEPT_CLIENT_OFFER + private_key_password: + description: Password for private key files (USE ONLY FOR TESTING) + type: string + default: "123456" + enable_ssl_logging: + description: Verbosely log the ssl/tls connection + type: boolean + default: false + enable_tls_key_logging: + description: >- + Enable/Disable the export of TLS session keys (pre-master-secret) + during a TLS handshake. Note that this option is for testing and + simulation purpose only + type: boolean + default: false + enable_sdp_server: + description: >- + Enable the built-in SDP server + type: boolean + default: true + supported_dynamic_mode: + description: The EVSE should support dynamic mode + type: boolean + default: true + supported_mobility_needs_mode_provided_by_secc: + description: >- + The EVSE should support the mobility needs mode provided by the SECC. + Mobility needs mode provided by the EVCC is always provided. + type: boolean + default: false + supported_scheduled_mode: + description: The EVSE should support scheduled mode + type: boolean + default: false +provides: + charger: + interface: ISO15118_charger + description: >- + This interface provides limited access to iso15118-20 +enable_external_mqtt: false +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - aw@pionix.de + - Sebastian Lukas diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index 1b8a63f1a..b17c44717 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -585,6 +585,19 @@ void Charger::run_state_machine() { } } + if (config_context.state_F_after_fault_ms > 0 and not shared_context.hlc_charging_active) { + // First time we see that a fatal error became active, signal F for a short time. + // Only use in basic charging mode. + if (entered_fatal_error_state()) { + pwm_F(); + } + + if (internal_context.pwm_F_active and + time_in_fatal_error_state_ms() > config_context.state_F_after_fault_ms) { + pwm_off(); + } + } + // Wait here until all errors are cleared if (stop_charging_on_fatal_error_internal()) { break; @@ -796,6 +809,7 @@ void Charger::process_event(CPEvent cp_event) { case CPEvent::CarRequestedStopPower: case CPEvent::CarUnplugged: case CPEvent::BCDtoEF: + case CPEvent::BCDtoE: case CPEvent::EFtoBCD: session_log.car(false, fmt::format("Event {}", cpevent_to_string(cp_event))); break; @@ -866,10 +880,10 @@ void Charger::process_cp_events_state(CPEvent cp_event) { signal_hlc_stop_charging(); session_log.evse(false, "CP state transition C->B at this stage violates ISO15118-2"); } - } else if (cp_event == CPEvent::BCDtoEF) { + } else if (cp_event == CPEvent::BCDtoE) { shared_context.iec_allow_close_contactor = false; shared_context.current_state = EvseState::StoppingCharging; - // Tell HLC stack to stop the session in case of an E/F event while charging. + // Tell HLC stack to stop the session in case of an E event while charging. if (shared_context.hlc_charging_active) { signal_hlc_stop_charging(); session_log.evse(false, "CP state transition C->E/F at this stage violates ISO15118-2"); @@ -958,7 +972,7 @@ void Charger::update_pwm_now(float dc) { "Set PWM On ({}%) took {} ms", dc * 100., (std::chrono::duration_cast(std::chrono::steady_clock::now() - start)).count())); internal_context.last_pwm_update = std::chrono::steady_clock::now(); - + internal_context.pwm_F_active = false; bsp->set_pwm(dc); } @@ -981,6 +995,7 @@ void Charger::pwm_off() { shared_context.pwm_running = false; internal_context.update_pwm_last_dc = 1.; internal_context.pwm_set_last_ampere = 0.; + internal_context.pwm_F_active = false; bsp->set_pwm_off(); } @@ -989,6 +1004,7 @@ void Charger::pwm_F() { shared_context.pwm_running = false; internal_context.update_pwm_last_dc = 0.; internal_context.pwm_set_last_ampere = 0.; + internal_context.pwm_F_active = true; bsp->set_pwm_F(); } @@ -1308,7 +1324,7 @@ void Charger::setup(bool has_ventilation, const ChargeMode _charge_mode, bool _a bool _ac_hlc_use_5percent, bool _ac_enforce_hlc, bool _ac_with_soc_timeout, float _soft_over_current_tolerance_percent, float _soft_over_current_measurement_noise_A, const int _switch_3ph1ph_delay_s, const std::string _switch_3ph1ph_cp_state, - const int _soft_over_current_timeout_ms) { + const int _soft_over_current_timeout_ms, const int _state_F_after_fault_ms) { // set up board support package bsp->setup(has_ventilation); @@ -1327,6 +1343,8 @@ void Charger::setup(bool has_ventilation, const ChargeMode _charge_mode, bool _a config_context.switch_3ph1ph_delay_s = _switch_3ph1ph_delay_s; config_context.switch_3ph1ph_cp_state_F = _switch_3ph1ph_cp_state == "F"; + config_context.state_F_after_fault_ms = _state_F_after_fault_ms; + if (config_context.charge_mode == ChargeMode::AC and config_context.ac_hlc_enabled) EVLOG_info << "AC HLC mode enabled."; } @@ -1868,14 +1886,31 @@ bool Charger::stop_charging_on_fatal_error() { return stop_charging_on_fatal_error_internal(); } +bool Charger::entered_fatal_error_state() { + return shared_context.error_prevent_charging_flag and not shared_context.last_error_prevent_charging_flag; +} + +int Charger::time_in_fatal_error_state_ms() { + if (not internal_context.fatal_error_timer_running) { + return 0; + } else { + return std::chrono::duration_cast(std::chrono::steady_clock::now() - + internal_context.fatal_error_became_active) + .count(); + } +} + bool Charger::stop_charging_on_fatal_error_internal() { bool err = false; if (shared_context.error_prevent_charging_flag) { if (not shared_context.last_error_prevent_charging_flag) { + internal_context.fatal_error_became_active = std::chrono::steady_clock::now(); + graceful_stop_charging(); } err = true; } + internal_context.fatal_error_timer_running = err; shared_context.last_error_prevent_charging_flag = shared_context.error_prevent_charging_flag; return err; } diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index c8cce467c..e2c5e5f1d 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -105,7 +105,8 @@ class Charger { void setup(bool has_ventilation, const ChargeMode charge_mode, bool ac_hlc_enabled, bool ac_hlc_use_5percent, bool ac_enforce_hlc, bool ac_with_soc_timeout, float soft_over_current_tolerance_percent, float soft_over_current_measurement_noise_A, const int switch_3ph1ph_delay_s, - const std::string switch_3ph1ph_cp_state, const int soft_over_current_timeout_ms); + const std::string switch_3ph1ph_cp_state, const int soft_over_current_timeout_ms, + const int _state_F_after_fault_ms); bool enable_disable(int connector_id, const types::evse_manager::EnableDisableSource& source); @@ -192,6 +193,8 @@ class Charger { void set_hlc_allow_close_contactor(bool on); bool stop_charging_on_fatal_error(); + bool entered_fatal_error_state(); + int time_in_fatal_error_state_ms(); /// @brief Returns the OCMF start data. /// @@ -322,6 +325,8 @@ class Charger { bool switch_3ph1ph_cp_state_F{false}; // Tolerate soft over current for given time int soft_over_current_timeout_ms{7000}; + // Switch to F for configured ms after a fatal error + int state_F_after_fault_ms{300}; } config_context; // Used by different threads, but requires no complete state machine locking @@ -362,6 +367,10 @@ class Charger { bool no_energy_warning_printed{false}; float pwm_set_last_ampere{0}; bool t_step_ef_x1_pause{false}; + bool pwm_F_active{false}; + + std::chrono::time_point fatal_error_became_active; + bool fatal_error_timer_running{false}; } internal_context; // main Charger thread diff --git a/modules/EvseManager/EnumFlags.hpp b/modules/EvseManager/EnumFlags.hpp deleted file mode 100644 index b2691fb2e..000000000 --- a/modules/EvseManager/EnumFlags.hpp +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest -#ifndef ENUMFLAGS_HPP -#define ENUMFLAGS_HPP - -#include -#include -#include - -namespace module { - -template struct AtomicEnumFlags { - static_assert(std::is_enum() == true, "Not enum"); - static_assert(std::is_integral() == true, "Not integer"); - static_assert((sizeof(B) * 8) >= static_cast(T::last) + 1, "Underlying flag type too small"); - std::atomic _value{0ULL}; - - constexpr std::size_t bit(const T& flag) const { - return 1ULL << static_cast>(flag); - } - - constexpr void set(const T& flag, bool value) { - if (value) { - set(flag); - } else { - reset(flag); - } - } - - constexpr void set(const T& flag) { - _value |= bit(flag); - } - - constexpr void reset(const T& flag) { - _value &= ~bit(flag); - } - - constexpr void reset() { - _value = 0ULL; - } - - constexpr bool all_reset() const { - return _value == 0ULL; - } - - constexpr bool is_set(const T& flag) const { - return (_value & bit(flag)) != 0; - } - - constexpr bool is_reset(const T& flag) const { - return (_value & bit(flag)) == 0; - } -}; - -} // namespace module -#endif diff --git a/modules/EvseManager/ErrorHandling.cpp b/modules/EvseManager/ErrorHandling.cpp index dc4c429a5..2f6ac049e 100644 --- a/modules/EvseManager/ErrorHandling.cpp +++ b/modules/EvseManager/ErrorHandling.cpp @@ -80,9 +80,9 @@ void ErrorHandling::clear_overcurrent_error() { // Find out if the current error set is fatal to charging or not void ErrorHandling::process_error() { const auto fatal = errors_prevent_charging(); - if (std::get(fatal)) { + if (fatal) { // signal to charger a new error has been set that prevents charging - raise_inoperative_error(std::get(fatal)); + raise_inoperative_error(*fatal); } else { // signal an error that does not prevent charging clear_inoperative_error(); @@ -110,56 +110,56 @@ void ErrorHandling::process_error() { } // Check all errors from p_evse and all requirements to see if they block charging -std::pair ErrorHandling::errors_prevent_charging() { +std::optional ErrorHandling::errors_prevent_charging() { - auto is_fatal = [](auto errors, auto ignore_list) -> std::pair { + auto is_fatal = [](auto errors, auto ignore_list) -> std::optional { for (const auto e : errors) { if (std::none_of(ignore_list.begin(), ignore_list.end(), [e](const auto& ign) { return e->type == ign; })) { - return {true, e->type}; + return e->type; } } - return {false, ""}; + return std::nullopt; }; auto fatal = is_fatal(p_evse->error_state_monitor->get_active_errors(), ignore_errors.evse); - if (std::get(fatal)) { + if (fatal) { return fatal; } fatal = is_fatal(r_bsp->error_state_monitor->get_active_errors(), ignore_errors.bsp); - if (std::get(fatal)) { + if (fatal) { return fatal; } if (r_connector_lock.size() > 0) { fatal = is_fatal(r_connector_lock[0]->error_state_monitor->get_active_errors(), ignore_errors.connector_lock); - if (std::get(fatal)) { + if (fatal) { return fatal; } } if (r_ac_rcd.size() > 0) { fatal = is_fatal(r_ac_rcd[0]->error_state_monitor->get_active_errors(), ignore_errors.ac_rcd); - if (std::get(fatal)) { + if (fatal) { return fatal; } } if (r_imd.size() > 0) { fatal = is_fatal(r_imd[0]->error_state_monitor->get_active_errors(), ignore_errors.imd); - if (std::get(fatal)) { + if (fatal) { return fatal; } } if (r_powersupply.size() > 0) { fatal = is_fatal(r_powersupply[0]->error_state_monitor->get_active_errors(), ignore_errors.powersupply); - if (std::get(fatal)) { + if (fatal) { return fatal; } } - return {false, ""}; + return std::nullopt; } void ErrorHandling::raise_inoperative_error(const std::string& caused_by) { @@ -169,7 +169,7 @@ void ErrorHandling::raise_inoperative_error(const std::string& caused_by) { } if (r_hlc.size() > 0) { - r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_Malfunction); + r_hlc[0]->call_send_error(types::iso15118_charger::EvseError::Error_EmergencyShutdown); } // raise externally diff --git a/modules/EvseManager/ErrorHandling.hpp b/modules/EvseManager/ErrorHandling.hpp index 72ee4ea1f..8db687eaf 100644 --- a/modules/EvseManager/ErrorHandling.hpp +++ b/modules/EvseManager/ErrorHandling.hpp @@ -19,6 +19,7 @@ #include #include +#include #include #include @@ -31,7 +32,6 @@ #include #include -#include "EnumFlags.hpp" #include "Timeout.hpp" #include "utils/thread.hpp" @@ -68,7 +68,7 @@ class ErrorHandling { void process_error(); void raise_inoperative_error(const std::string& caused_by); void clear_inoperative_error(); - std::pair errors_prevent_charging(); + std::optional errors_prevent_charging(); const std::unique_ptr& r_bsp; const std::vector>& r_hlc; diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 13730d5bf..b269d19bf 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; @@ -243,21 +243,28 @@ void EvseManager::ready() { auto sae_mode = types::iso15118_charger::SaeJ2847BidiMode::None; // Set up energy transfer modes for HLC. For now we only support either DC or AC, not both at the same time. - std::vector transfer_modes; + std::vector transfer_modes; if (config.charge_mode == "AC") { types::iso15118_charger::SetupPhysicalValues setup_physical_values; setup_physical_values.ac_nominal_voltage = config.ac_nominal_voltage; r_hlc[0]->call_set_charging_parameters(setup_physical_values); + constexpr auto support_bidi = false; + // FIXME: we cannot change this during run time at the moment. Refactor ISO interface to exclude transfer // modes from setup // transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::AC_single_phase_core); - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::AC_three_phase_core); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::AC_three_phase_core, support_bidi}); } else if (config.charge_mode == "DC") { - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::DC_extended); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_extended, false}); + + const auto caps = get_powersupply_capabilities(); + update_powersupply_capabilities(caps); - update_powersupply_capabilities(get_powersupply_capabilities()); + if (caps.bidirectional) { + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_extended, true}); + } // Set present measurements on HLC to sane defaults types::iso15118_charger::DcEvsePresentVoltageCurrent present_values; @@ -355,6 +362,18 @@ void EvseManager::ready() { if (config.hack_skoda_enyaq and (v.dc_ev_target_voltage < 300 or v.dc_ev_target_current < 0)) return; + // Limit voltage/current for broken EV implementations + const auto ev = get_ev_info(); + if (ev.maximum_current_limit.has_value() and + v.dc_ev_target_current > ev.maximum_current_limit.value()) { + v.dc_ev_target_current = ev.maximum_current_limit.value(); + } + + if (ev.maximum_voltage_limit.has_value() and + v.dc_ev_target_voltage > ev.maximum_voltage_limit.value()) { + v.dc_ev_target_voltage = ev.maximum_voltage_limit.value(); + } + if (v.dc_ev_target_voltage not_eq latest_target_voltage or v.dc_ev_target_current not_eq latest_target_current) { latest_target_voltage = v.dc_ev_target_voltage; @@ -378,6 +397,48 @@ void EvseManager::ready() { } }); + r_hlc[0]->subscribe_d20_dc_dynamic_charge_mode( + [this](types::iso15118_charger::DcChargeDynamicModeValues values) { + constexpr auto PRE_CHARGE_MAX_POWER = 800.0f; + + bool target_changed{false}; + + if (values.min_voltage > latest_target_voltage) { + latest_target_voltage = values.min_voltage + 10; // TODO(sl): Check if okay + target_changed = true; + } + if (values.max_voltage < latest_target_voltage) { + latest_target_voltage = values.max_voltage - 10; // TODO(sl): Check if okay + target_changed = true; + } + + const double latest_target_power = latest_target_voltage * latest_target_current; + + if (latest_target_power <= PRE_CHARGE_MAX_POWER or values.min_charge_power > latest_target_power or + values.max_charge_power < latest_target_power) { + latest_target_current = static_cast(values.max_charge_power) / latest_target_voltage; + if (values.max_charge_current < latest_target_current) { + latest_target_current = values.max_charge_current; + } + target_changed = true; + } + + if (target_changed) { + apply_new_target_voltage_current(); + if (not contactor_open) { + powersupply_DC_on(); + } + + { + Everest::scoped_lock_timeout lock(ev_info_mutex, + Everest::MutexDescription::EVSE_publish_ev_info); + ev_info.target_voltage = latest_target_voltage; + ev_info.target_current = latest_target_current; + p_evse->publish_ev_info(ev_info); + } + } + }); + // Car requests DC contactor open. We don't actually open but switch off DC supply. // opening will be done by Charger on C->B CP event. r_hlc[0]->subscribe_dc_open_contactor([this] { @@ -594,10 +655,8 @@ void EvseManager::ready() { // Install debug V2G Messages handler if session logging is enabled if (config.session_logging) { - r_hlc[0]->subscribe_v2g_messages([this](types::iso15118_charger::V2gMessages v2g_messages) { - json v2g = v2g_messages; - log_v2g_message(v2g); - }); + r_hlc[0]->subscribe_v2g_messages( + [this](types::iso15118_charger::V2gMessages v2g_messages) { log_v2g_message(v2g_messages); }); r_hlc[0]->subscribe_selected_protocol( [this](std::string selected_protocol) { this->selected_protocol = selected_protocol; }); @@ -644,8 +703,12 @@ void EvseManager::ready() { car_manufacturer = types::evse_manager::CarManufacturer::Unknown; r_slac[0]->call_enter_bcd(); } else if (event == CPEvent::CarUnplugged) { + // Make a local copy as leave_bcd() will overwrite the slac_unmatched flag + bool unmatched_on_unplug = not slac_unmatched; r_slac[0]->call_leave_bcd(); - r_slac[0]->call_reset(false); + if (unmatched_on_unplug) { + r_slac[0]->call_reset(false); + } } } @@ -737,8 +800,10 @@ void EvseManager::ready() { // Notify charger whether matching was started (or is done) or not if (s == "UNMATCHED") { charger->set_matching_started(false); + slac_unmatched = true; } else { charger->set_matching_started(true); + slac_unmatched = false; } }); @@ -818,11 +883,12 @@ void EvseManager::ready() { if (config.ac_with_soc) { setup_fake_DC_mode(); } else { - charger->setup( - config.has_ventilation, (config.charge_mode == "DC" ? Charger::ChargeMode::DC : Charger::ChargeMode::AC), - hlc_enabled, config.ac_hlc_use_5percent, config.ac_enforce_hlc, false, - config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, - config.switch_3ph1ph_delay_s, config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms); + charger->setup(config.has_ventilation, + (config.charge_mode == "DC" ? Charger::ChargeMode::DC : Charger::ChargeMode::AC), hlc_enabled, + config.ac_hlc_use_5percent, config.ac_enforce_hlc, false, + config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, + config.switch_3ph1ph_delay_s, config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, + config.state_F_after_fault_ms); } telemetryThreadHandle = std::thread([this]() { @@ -1008,17 +1074,18 @@ void EvseManager::setup_fake_DC_mode() { charger->setup(config.has_ventilation, Charger::ChargeMode::DC, hlc_enabled, config.ac_hlc_use_5percent, config.ac_enforce_hlc, false, config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s, - config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms); + config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms); types::iso15118_charger::EVSEID evseid = {config.evse_id, config.evse_id_din}; // Set up energy transfer modes for HLC. For now we only support either DC or AC, not both at the same time. - std::vector transfer_modes; + std::vector transfer_modes; + constexpr auto support_bidi = false; - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::DC_core); - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::DC_extended); - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::DC_combo_core); - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::DC_unique); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_core, support_bidi}); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_extended, support_bidi}); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_combo_core, support_bidi}); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::DC_unique, support_bidi}); types::iso15118_charger::DcEvsePresentVoltageCurrent present_values; present_values.evse_present_voltage = 400; // FIXME: set a correct values @@ -1037,7 +1104,7 @@ void EvseManager::setup_fake_DC_mode() { evse_min_limits.evse_minimum_voltage_limit = 0; r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits); - const auto sae_mode = types::iso15118_charger::SaeJ2847BidiMode::None; + constexpr auto sae_mode = types::iso15118_charger::SaeJ2847BidiMode::None; r_hlc[0]->call_setup(evseid, transfer_modes, sae_mode, config.session_logging); } @@ -1046,22 +1113,23 @@ void EvseManager::setup_AC_mode() { charger->setup(config.has_ventilation, Charger::ChargeMode::AC, hlc_enabled, config.ac_hlc_use_5percent, config.ac_enforce_hlc, true, config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s, - config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms); + config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms); types::iso15118_charger::EVSEID evseid = {config.evse_id, config.evse_id_din}; // Set up energy transfer modes for HLC. For now we only support either DC or AC, not both at the same time. - std::vector transfer_modes; + std::vector transfer_modes; + constexpr auto support_bidi = false; - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::AC_single_phase_core); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::AC_single_phase_core, support_bidi}); if (get_hw_capabilities().max_phase_count_import == 3) { - transfer_modes.push_back(types::iso15118_charger::EnergyTransferMode::AC_three_phase_core); + transfer_modes.push_back({types::iso15118_charger::EnergyTransferMode::AC_three_phase_core, support_bidi}); } types::iso15118_charger::SetupPhysicalValues setup_physical_values; - const auto sae_mode = types::iso15118_charger::SaeJ2847BidiMode::None; + constexpr auto sae_mode = types::iso15118_charger::SaeJ2847BidiMode::None; if (get_hlc_enabled()) { r_hlc[0]->call_setup(evseid, transfer_modes, sae_mode, config.session_logging); @@ -1089,6 +1157,8 @@ void EvseManager::setup_v2h_mode() { powersupply_capabilities.min_import_voltage_V.has_value()) { evse_min_limits.evse_minimum_current_limit = -powersupply_capabilities.min_import_current_A.value(); evse_min_limits.evse_minimum_voltage_limit = powersupply_capabilities.min_import_voltage_V.value(); + evse_min_limits.evse_minimum_power_limit = + evse_min_limits.evse_minimum_current_limit * evse_min_limits.evse_minimum_voltage_limit; r_hlc[0]->call_update_dc_minimum_limits(evse_min_limits); } else { EVLOG_error << "No Import Current, Power or Voltage is available!!!"; @@ -1174,34 +1244,56 @@ 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 && this->reservation_id != -1) { + EVLOG_info << "Rejecting reservation because evse is already reserved for reservation id " + << this->reservation_id; + } + + // Check if this evse is not already reserved, or overwrite reservation if it is for the same reservation id. + if (not reserved || this->reservation_id == -1 || overwrite_reservation) { + EVLOG_debug << "Make the reservation with id " << id; reserved = true; - reservation_id = id; + if (id >= 0) { + this->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 || this->reservation_id == -1) && 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; } @@ -1212,8 +1304,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; + this->reservation_id = -1; // publish event to other modules if (signal_event) { @@ -1239,24 +1332,19 @@ bool EvseManager::get_hlc_waiting_for_auth_pnc() { return hlc_waiting_for_auth_pnc; } -void EvseManager::log_v2g_message(Object m) { - std::string msg = m["v2g_message_id"]; +void EvseManager::log_v2g_message(types::iso15118_charger::V2gMessages v2g_messages) { - std::string xml = ""; - std::string json_str = ""; - if (m["v2g_message_xml"].is_null() and m["v2g_message_json"].is_string()) { - json_str = m["v2g_message_json"]; - } else if (m["v2g_message_xml"].is_string()) { - xml = m["v2g_message_xml"]; - } + const std::string msg = types::iso15118_charger::v2g_message_id_to_string(v2g_messages.id); + const std::string xml = v2g_messages.xml.value_or(""); + const std::string json_str = v2g_messages.v2g_json.value_or(""); + const std::string exi_hex = v2g_messages.exi.value_or(""); + const std::string exi_base64 = v2g_messages.exi_base64.value_or(""); // All messages from EVSE contain Req and all originating from Car contain Res if (msg.find("Res") == std::string::npos) { - session_log.car(true, fmt::format("V2G {}", msg), xml, m["v2g_message_exi_hex"], m["v2g_message_exi_base64"], - json_str); + session_log.car(true, fmt::format("V2G {}", msg), xml, exi_hex, exi_base64, json_str); } else { - session_log.evse(true, fmt::format("V2G {}", msg), xml, m["v2g_message_exi_hex"], m["v2g_message_exi_base64"], - json_str); + session_log.evse(true, fmt::format("V2G {}", msg), xml, exi_hex, exi_base64, json_str); } } @@ -1462,32 +1550,37 @@ void EvseManager::cable_check() { // CC.4.1.4: Perform the insulation resistance check imd_start(); - // read out new isolation resistance value - isolation_measurement.clear(); - types::isolation_monitor::IsolationMeasurement m; + if (config.cable_check_wait_number_of_imd_measurements > 0) { + // read out new isolation resistance value + isolation_measurement.clear(); + types::isolation_monitor::IsolationMeasurement m; + + EVLOG_info << "CableCheck: Waiting for " << config.cable_check_wait_number_of_imd_measurements + << " isolation measurement sample(s)"; + // Wait for N isolation measurement values + for (int i = 0; i < config.cable_check_wait_number_of_imd_measurements; i++) { + if (not isolation_measurement.wait_for(m, 5s) or cable_check_should_exit()) { + EVLOG_info << "Did not receive isolation measurement from IMD within 5 seconds."; + imd_stop(); + fail_cable_check(); + return; + } + } - EVLOG_info << "CableCheck: Waiting for " << config.cable_check_wait_number_of_imd_measurements - << " isolation measurement sample(s)"; - // Wait for N isolation measurement values - for (int i = 0; i < config.cable_check_wait_number_of_imd_measurements; i++) { - if (not isolation_measurement.wait_for(m, 5s) or cable_check_should_exit()) { - EVLOG_info << "Did not receive isolation measurement from IMD within 5 seconds."; + charger->get_stopwatch().mark("Measure"); + + // Now the value is valid and can be trusted. + // Verify it is within ranges. Fault is <100 kOhm + // Note that 2023 edition removed the warning level which was included in the 2014 edition. + // Refer to IEC 61851-23 (2023) 6.3.1.105 and CC.4.1.2 / CC.4.1.4 + if (not check_isolation_resistance_in_range(m.resistance_F_Ohm)) { imd_stop(); fail_cable_check(); return; } - } - - charger->get_stopwatch().mark("Measure"); - - // Now the value is valid and can be trusted. - // Verify it is within ranges. Fault is <100 kOhm - // Note that 2023 edition removed the warning level which was included in the 2014 edition. - // Refer to IEC 61851-23 (2023) 6.3.1.105 and CC.4.1.2 / CC.4.1.4 - if (not check_isolation_resistance_in_range(m.resistance_F_Ohm)) { - imd_stop(); - fail_cable_check(); - return; + } else { + // If no measurements are needed after self test, report valid isolation status to ISO stack + r_hlc[0]->call_update_isolation_status(types::iso15118_charger::IsolationStatus::Valid); } // We are done with the isolation measurement and can now report success to the EV, @@ -1507,18 +1600,20 @@ void EvseManager::cable_check() { charger->get_stopwatch().mark("Sleep"); } - // CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV - powersupply_DC_off(); + if (config.cable_check_wait_below_60V_before_finish) { + // CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV + powersupply_DC_off(); - if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { - EVLOG_error << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout."; - imd_stop(); - fail_cable_check(); - return; - } - charger->get_stopwatch().mark("VRampDown"); + if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { + EVLOG_error << "Voltage did not drop below " << CABLECHECK_SAFE_VOLTAGE << "V within timeout."; + imd_stop(); + fail_cable_check(); + return; + } + charger->get_stopwatch().mark("VRampDown"); - EVLOG_info << "CableCheck done, output is below " << CABLECHECK_SAFE_VOLTAGE << "V"; + EVLOG_info << "CableCheck done, output is below " << CABLECHECK_SAFE_VOLTAGE << "V"; + } // Report CableCheck Finished with success to EV r_hlc[0]->call_cable_check_finished(true); @@ -1566,7 +1661,7 @@ bool EvseManager::powersupply_DC_set(double _voltage, double _current) { if (((config.hack_allow_bpt_with_iso2 or config.sae_j2847_2_bpt_enabled) and current_demand_active) and is_actually_exporting_to_grid) { - if (not last_is_actually_exporting_to_grid) { + if (not last_is_actually_exporting_to_grid and powersupply_dc_is_on) { // switching from import from grid to export to grid session_log.evse(false, "DC power supply: switch ON in import mode"); r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Import, power_supply_DC_charging_phase); @@ -1599,9 +1694,10 @@ bool EvseManager::powersupply_DC_set(double _voltage, double _current) { return false; } else { - if (charging_phase_changed or (((config.hack_allow_bpt_with_iso2 or config.sae_j2847_2_bpt_enabled) and - last_is_actually_exporting_to_grid) and - current_demand_active)) { + if (powersupply_dc_is_on and + (charging_phase_changed or (((config.hack_allow_bpt_with_iso2 or config.sae_j2847_2_bpt_enabled) and + last_is_actually_exporting_to_grid) and + current_demand_active))) { // switching from export to grid to import from grid session_log.evse(false, "DC power supply: switch ON in export mode"); r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Export, power_supply_DC_charging_phase); @@ -1635,12 +1731,12 @@ bool EvseManager::powersupply_DC_set(double _voltage, double _current) { } void EvseManager::powersupply_DC_off() { - power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; if (powersupply_dc_is_on) { session_log.evse(false, "DC power supply OFF"); r_powersupply_DC[0]->call_setMode(types::power_supply_DC::Mode::Off, power_supply_DC_charging_phase); powersupply_dc_is_on = false; } + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; } bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) { @@ -1651,6 +1747,7 @@ bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) { while (not timeout.reached()) { if (cable_check_should_exit()) { EVLOG_warning << "Cancel cable check wait voltage reached"; + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; powersupply_DC_off(); r_hlc[0]->call_cable_check_finished(false); charger->set_hlc_error(); @@ -1665,6 +1762,7 @@ bool EvseManager::wait_powersupply_DC_voltage_reached(double target_voltage) { } } else { EVLOG_info << "Did not receive voltage measurement from power supply within 2 seconds."; + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; powersupply_DC_off(); break; } @@ -1680,6 +1778,7 @@ bool EvseManager::wait_powersupply_DC_below_voltage(double target_voltage) { while (not timeout.reached()) { if (cable_check_should_exit()) { EVLOG_warning << "Cancel cable check wait below voltage"; + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; powersupply_DC_off(); r_hlc[0]->call_cable_check_finished(false); charger->set_hlc_error(); @@ -1694,6 +1793,7 @@ bool EvseManager::wait_powersupply_DC_below_voltage(double target_voltage) { } } else { EVLOG_info << "Did not receive voltage measurement from power supply within 2 seconds."; + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; powersupply_DC_off(); break; } @@ -1724,7 +1824,6 @@ void EvseManager::imd_start() { // This returns our active local limits, which is either externally set limits // or hardware capabilties types::energy::ExternalLimits EvseManager::get_local_energy_limits() { - types::energy::ExternalLimits active_local_limits; std::scoped_lock lock(external_local_limits_mutex); @@ -1748,6 +1847,7 @@ types::energy::ExternalLimits EvseManager::get_local_energy_limits() { void EvseManager::fail_cable_check() { if (config.charge_mode == "DC") { + power_supply_DC_charging_phase = types::power_supply_DC::ChargingPhase::Other; powersupply_DC_off(); // CC.4.1.2: We need to wait until voltage is below 60V before sending a CableCheck Finished to the EV if (not wait_powersupply_DC_below_voltage(CABLECHECK_SAFE_VOLTAGE)) { diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index adc42f8aa..fc4ba9825 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; @@ -75,6 +76,7 @@ struct Conf { int hack_sleep_in_cable_check_volkswagen; int cable_check_wait_number_of_imd_measurements; bool cable_check_enable_imd_self_test; + bool cable_check_wait_below_60V_before_finish; bool hack_skoda_enyaq; int hack_present_current_offset; bool hack_pause_imd_during_precharge; @@ -99,6 +101,7 @@ struct Conf { std::string switch_3ph1ph_cp_state; int soft_over_current_timeout_ms; bool lock_connector_in_state_b; + int state_F_after_fault_ms; }; class EvseManager : public Everest::ModuleBase { @@ -133,7 +136,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; @@ -171,7 +175,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(); @@ -226,11 +238,13 @@ class EvseManager : public Everest::ModuleBase { types::iso15118_charger::DcEvseMinimumLimits evse_min_limits; evse_min_limits.evse_minimum_current_limit = powersupply_capabilities.min_export_current_A; evse_min_limits.evse_minimum_voltage_limit = powersupply_capabilities.min_export_voltage_V; + evse_min_limits.evse_minimum_power_limit = + evse_min_limits.evse_minimum_current_limit * evse_min_limits.evse_minimum_voltage_limit; 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. @@ -289,7 +303,7 @@ class EvseManager : public Everest::ModuleBase { types::authorization::ProvidedIdToken autocharge_token; - void log_v2g_message(Object m); + void log_v2g_message(types::iso15118_charger::V2gMessages v2g_messages); // Reservations bool reserved; @@ -338,6 +352,7 @@ class EvseManager : public Everest::ModuleBase { static constexpr int CABLECHECK_SELFTEST_TIMEOUT{30}; std::atomic_bool current_demand_active{false}; + std::atomic_bool slac_unmatched{false}; std::mutex powermeter_mutex; std::condition_variable powermeter_cv; bool initial_powermeter_value_received{false}; diff --git a/modules/EvseManager/IECStateMachine.cpp b/modules/EvseManager/IECStateMachine.cpp index 6964b4ba0..4bdb8898f 100644 --- a/modules/EvseManager/IECStateMachine.cpp +++ b/modules/EvseManager/IECStateMachine.cpp @@ -69,6 +69,8 @@ const std::string cpevent_to_string(CPEvent e) { return "EFtoBCD"; case CPEvent::BCDtoEF: return "BCDtoEF"; + case CPEvent::BCDtoE: + return "BCDtoE"; case CPEvent::EvseReplugStarted: return "EvseReplugStarted"; case CPEvent::EvseReplugFinished: @@ -82,6 +84,7 @@ IECStateMachine::IECStateMachine(const std::unique_ptr& r_bsp(r_bsp_), lock_connector_in_state_b(lock_connector_in_state_b_) { // feed the state machine whenever the timer expires timeout_state_c1.signal_reached.connect(&IECStateMachine::feed_state_machine_no_thread, this); + timeout_unlock_state_F.signal_reached.connect(&IECStateMachine::feed_state_machine_no_thread, this); // Subscribe to bsp driver to receive BspEvents from the hardware r_bsp->subscribe_event([this](const types::board_support_common::BspEvent event) { @@ -142,13 +145,18 @@ void IECStateMachine::feed_state_machine_no_thread() { std::queue IECStateMachine::state_machine() { std::queue events; - auto timer = TimerControl::do_nothing; + auto timer_state_C1 = TimerControl::do_nothing; + auto timer_unlock_state_F = TimerControl::do_nothing; { // mutex protected section Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_state_machine); + if (cp_state not_eq RawCPState::F and last_cp_state == RawCPState::F) { + timer_unlock_state_F = TimerControl::stop; + } + switch (cp_state) { case RawCPState::Disabled: @@ -156,7 +164,7 @@ std::queue IECStateMachine::state_machine() { pwm_running = false; r_bsp->call_pwm_off(); ev_simplified_mode = false; - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); connector_unlock(); } @@ -169,7 +177,7 @@ std::queue IECStateMachine::state_machine() { ev_simplified_mode = false; car_plugged_in = false; call_allow_power_on_bsp(false); - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; connector_unlock(); } @@ -196,13 +204,14 @@ std::queue IECStateMachine::state_machine() { // Need to switch off according to Table A.6 Sequence 8.1 // within 100ms call_allow_power_on_bsp(false); - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; } // Table A.6: Sequence 1.1 Plug-in if (last_cp_state == RawCPState::A || last_cp_state == RawCPState::Disabled || (!car_plugged_in && last_cp_state == RawCPState::F)) { events.push(CPEvent::CarPluggedIn); + car_plugged_in = true; ev_simplified_mode = false; } @@ -217,7 +226,7 @@ std::queue IECStateMachine::state_machine() { // If state D is not supported switch off. if (not has_ventilation) { call_allow_power_on_bsp(false); - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; break; } // no break, intended fall through: If we support state D it is handled the same way as state C @@ -229,6 +238,7 @@ std::queue IECStateMachine::state_machine() { if (last_cp_state == RawCPState::A || last_cp_state == RawCPState::Disabled || (!car_plugged_in && last_cp_state == RawCPState::F)) { events.push(CPEvent::CarPluggedIn); + car_plugged_in = true; EVLOG_info << "Detected simplified mode."; ev_simplified_mode = true; } else if (last_cp_state == RawCPState::B) { @@ -238,13 +248,13 @@ std::queue IECStateMachine::state_machine() { if (!pwm_running && last_pwm_running) { // X2->C1 // Table A.6 Sequence 10.2: EV does not stop drawing power // even if PWM stops. Stop within 6 seconds (E.g. Kona1!) - timer = TimerControl::start; + timer_state_C1 = TimerControl::start; } // PWM switches on while in state C if (pwm_running && !last_pwm_running) { // when resuming after a pause before the EV goes to state B, stop the timer. - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; // If we resume charging and the EV never left state C during pause we allow non-compliant EVs to switch // on again. @@ -285,27 +295,32 @@ std::queue IECStateMachine::state_machine() { case RawCPState::E: connector_unlock(); if (last_cp_state != RawCPState::E) { - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); pwm_running = false; r_bsp->call_pwm_off(); if (last_cp_state == RawCPState::B || last_cp_state == RawCPState::C || last_cp_state == RawCPState::D) { events.push(CPEvent::BCDtoEF); + events.push(CPEvent::BCDtoE); } } break; case RawCPState::F: - connector_unlock(); - timer = TimerControl::stop; + timer_state_C1 = TimerControl::stop; call_allow_power_on_bsp(false); if (last_cp_state not_eq RawCPState::F) { + timer_unlock_state_F = TimerControl::start; pwm_running = false; } if (last_cp_state == RawCPState::B || last_cp_state == RawCPState::C || last_cp_state == RawCPState::D) { events.push(CPEvent::BCDtoEF); } + + if (timeout_unlock_state_F.reached()) { + connector_unlock(); + } break; } @@ -320,9 +335,9 @@ std::queue IECStateMachine::state_machine() { // stopping the timer could lead to a deadlock when called from the // mutex protected section - switch (timer) { + switch (timer_state_C1) { case TimerControl::start: - timeout_state_c1.start(std::chrono::seconds(6)); + timeout_state_c1.start(power_off_under_load_in_c1_timeout); break; case TimerControl::stop: timeout_state_c1.stop(); @@ -332,6 +347,18 @@ std::queue IECStateMachine::state_machine() { break; } + switch (timer_unlock_state_F) { + case TimerControl::start: + timeout_unlock_state_F.start(unlock_in_state_f_timeout); + break; + case TimerControl::stop: + timeout_unlock_state_F.stop(); + break; + case TimerControl::do_nothing: + default: + break; + } + return events; } @@ -381,7 +408,6 @@ void IECStateMachine::set_pwm_F() { // The higher level state machine in Charger.cpp calls this to indicate it allows contactors to be switched on void IECStateMachine::allow_power_on(bool value, types::evse_board_support::Reason reason) { - EVLOG_info << "Allow power on: " << value << " Reason: " << reason; { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::IEC_allow_power_on); // Only set the flags here in case of power on. @@ -479,6 +505,13 @@ void IECStateMachine::connector_force_unlock() { cp = cp_state; } + if (not relais_on) { + // Unconditionally try to unlock, as `is_locked` might not always reflect the physical state of the lock. + // This can occur for example in case of a failed unlock due to a hardware issue. + signal_unlock(); + is_locked = false; + } + if (cp == RawCPState::B or cp == RawCPState::C) { force_unlocked = true; check_connector_lock(); @@ -486,10 +519,12 @@ void IECStateMachine::connector_force_unlock() { } void IECStateMachine::check_connector_lock() { - if (should_be_locked and not force_unlocked and not is_locked) { + bool should_be_locked_considering_relais_and_force = relais_on or (should_be_locked and not force_unlocked); + + if (not is_locked and should_be_locked_considering_relais_and_force) { signal_lock(); is_locked = true; - } else if ((not should_be_locked or force_unlocked) and is_locked and not relais_on) { + } else if (is_locked and not should_be_locked_considering_relais_and_force) { signal_unlock(); is_locked = false; } diff --git a/modules/EvseManager/IECStateMachine.hpp b/modules/EvseManager/IECStateMachine.hpp index d05b401b8..7236b6354 100644 --- a/modules/EvseManager/IECStateMachine.hpp +++ b/modules/EvseManager/IECStateMachine.hpp @@ -46,6 +46,7 @@ enum class CPEvent { CarUnplugged, EFtoBCD, BCDtoEF, + BCDtoE, EvseReplugStarted, EvseReplugFinished, }; @@ -125,6 +126,7 @@ class IECStateMachine { RawCPState cp_state{RawCPState::Disabled}, last_cp_state{RawCPState::Disabled}; AsyncTimeout timeout_state_c1; + AsyncTimeout timeout_unlock_state_F; Everest::timed_mutex_traceable state_machine_mutex; void feed_state_machine(); @@ -140,6 +142,9 @@ class IECStateMachine { std::atomic_bool enabled{false}; std::atomic_bool relais_on{false}; + + static constexpr std::chrono::seconds power_off_under_load_in_c1_timeout{6}; + static constexpr std::chrono::seconds unlock_in_state_f_timeout{5}; }; } // namespace module diff --git a/modules/EvseManager/Timeout.hpp b/modules/EvseManager/Timeout.hpp index 0e9897237..3891fde3f 100644 --- a/modules/EvseManager/Timeout.hpp +++ b/modules/EvseManager/Timeout.hpp @@ -51,58 +51,71 @@ class Timeout { class AsyncTimeout { public: void start(milliseconds _t) { + std::scoped_lock lock(mutex); + if (running) { - stop(); + wait_thread.stop(); + running = false; } + t = _t; start_time = steady_clock::now(); - running = true; + // start waiting thread wait_thread = std::thread([this]() { while (not wait_thread.shouldExit()) { std::this_thread::sleep_for(resolution); - if (reached()) { + if (reached_nolock()) { // Note the order is important here. // We first signal reached which will call all callbacks. - // The timer is still running in this in those callbacks, so they can also call reached() and get a - // true as return value. + // The timer is still running in this in those callbacks, so they can also call reached() and + // get a true as return value. signal_reached(); // After all signal handlers are called, we stop the timer. - running = false; - return; + break; } } }); + running = true; } // Note that stopping the timer may take up to "resolution" amount of time to return void stop() { - wait_thread.stop(); - running = false; + std::scoped_lock lock(mutex); + if (running) { + wait_thread.stop(); + running = false; + } } bool is_running() { + std::scoped_lock lock(mutex); return running; } bool reached() { + std::scoped_lock lock(mutex); + return reached_nolock(); + } + + sigslot::signal<> signal_reached; + +private: + bool reached_nolock() { if (!running) { return false; - } - if ((steady_clock::now() - start_time) > t) { + } else if ((steady_clock::now() - start_time) > t) { return true; } else { return false; } } - sigslot::signal<> signal_reached; - -private: constexpr static auto resolution = 500ms; milliseconds t; time_point start_time; - std::atomic_bool running{false}; + bool running{false}; + std::mutex mutex; Everest::Thread wait_thread; }; diff --git a/modules/EvseManager/doc.rst b/modules/EvseManager/doc.rst index 08687f0da..4fb00fc31 100644 --- a/modules/EvseManager/doc.rst +++ b/modules/EvseManager/doc.rst @@ -224,67 +224,45 @@ The control flow of this module can be influenced by the error implementation of the side effects that can be caused by errors raised by a requirement. This module subscribes to all errors of the following requirements: + * evse_board_support * connector_lock * ac_rcd * isolation_monitor +* power_supply_DC A raised error can cause the EvseManager to become Inoperative. This means that charging is not possible until the error is cleared. If no charging session is currently running, it will prevent sessions from being started. If a charging session is currently running and an error is raised this will interrupt the charging session. -The following sections provide an overview of errors that cause the EvseManager to become Inoperative until the error is cleared. +Almost all errors that are reported from the requirements of this module cause the EvseManager to become Inoperative until the error is cleared. +The following sections provide an overview of the errors that do **not** cause the EvseManager to become Inoperative. evse_board_support ------------------ -evse_board_support/DiodeFault -evse_board_support/VentilationNotAvailable -evse_board_support/BrownOut -evse_board_support/EnergyManagement -evse_board_support/PermanentFault -evse_board_support/MREC2GroundFailure -evse_board_support/MREC4OverCurrentFailure -evse_board_support/MREC5OverVoltage -evse_board_support/MREC6UnderVoltage -evse_board_support/MREC8EmergencyStop -evse_board_support/MREC10InvalidVehicleMode -evse_board_support/MREC14PilotFault -evse_board_support/MREC15PowerLoss -evse_board_support/MREC17EVSEContactorFault -evse_board_support/MREC19CableOverTempStop -evse_board_support/MREC20PartialInsertion -evse_board_support/MREC23ProximityFault -evse_board_support/MREC24ConnectorVoltageHigh -evse_board_support/MREC25BrokenLatch -evse_board_support/MREC26CutCable -evse_board_support/VendorError -evse_board_support/CommunicationFault +* evse_board_support/MREC3HighTemperature +* evse_board_support/MREC18CableOverTempDerate +* evse_board_support/VendorWarning connector_lock -------------- -connector_lock/ConnectorLockCapNotCharged -connector_lock/ConnectorLockUnexpectedClose -connector_lock/ConnectorLockUnexpectedOpen -connector_lock/ConnectorLockFailedLock -connector_lock/ConnectorLockFailedUnlock -connector_lock/MREC1ConnectorLockFailure -connector_lock/VendorError +* connector_lock/VendorWarning ac_rcd ------ -ac_rcd/MREC2GroundFailure -ac_rcd/VendorError -ac_rcd/Selftest -ac_rcd/AC -ac_rcd/DC +* ac_rcd/VendorWarning isolation_monitor ----------------- -isolation_monitor/DeviceFault -isolation_monitor/CommunicationFault -isolation_monitor/VendorError +* isolation_monitor/VendorWarning + +power_supply_DC +--------------- + +* power_supply_DC/VendorWarning + diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index 168b678c5..e1ba78a83 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -112,10 +112,14 @@ void evse_managerImpl::init() { // /Interface to Node-RED debug UI if (mod->r_powermeter_billing().size() > 0) { - mod->r_powermeter_billing()[0]->subscribe_powermeter([this](const types::powermeter::Powermeter p) { + mod->r_powermeter_billing()[0]->subscribe_powermeter([this](const types::powermeter::Powermeter& p) { // Republish data on proxy powermeter struct publish_powermeter(p); }); + mod->r_powermeter_billing()[0]->subscribe_public_key_ocmf([this](const std::string& public_key_ocmf) { + // Republish data on proxy powermeter public_key_ocmf + publish_powermeter_public_key_ocmf(public_key_ocmf); + }); } } @@ -171,6 +175,9 @@ void evse_managerImpl::ready() { session_started.id_tag = provided_id_token; if (mod->is_reserved()) { session_started.reservation_id = mod->get_reservation_id(); + if (start_reason == types::evse_manager::StartSessionReason::Authorized) { + this->mod->cancel_reservation(true); + } } session_started.logging_path = session_log.startSession( @@ -202,7 +209,7 @@ void evse_managerImpl::ready() { transaction_started.meter_value = mod->get_latest_powermeter_data_billing(); if (mod->is_reserved()) { transaction_started.reservation_id.emplace(mod->get_reservation_id()); - mod->cancel_reservation(false); + mod->cancel_reservation(true); } transaction_started.id_tag = id_token; @@ -285,6 +292,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(true); + } session_log.stopSession(); mod->telemetry.publish("session", "events", {{"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, @@ -353,11 +365,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; } @@ -380,6 +401,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) { @@ -394,7 +425,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() { @@ -417,10 +448,6 @@ bool evse_managerImpl::handle_stop_transaction(types::evse_manager::StopTransact return mod->charger->cancel_transaction(request); }; -void evse_managerImpl::handle_set_external_limits(types::energy::ExternalLimits& value) { - mod->update_local_energy_limit(value); -} - void evse_managerImpl::handle_set_get_certificate_response( types::iso15118_charger::ResponseExiStreamStatus& certificate_reponse) { mod->r_hlc[0]->call_certificate_response(certificate_reponse); diff --git a/modules/EvseManager/evse/evse_managerImpl.hpp b/modules/EvseManager/evse/evse_managerImpl.hpp index f9db90dad..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 @@ -47,7 +48,6 @@ class evse_managerImpl : public evse_managerImplBase { virtual bool handle_resume_charging() override; virtual bool handle_stop_transaction(types::evse_manager::StopTransactionRequest& request) override; virtual bool handle_force_unlock(int& connector_id) override; - virtual void handle_set_external_limits(types::energy::ExternalLimits& value) override; virtual void handle_set_get_certificate_response( types::iso15118_charger::ResponseExiStreamStatus& certificate_response) override; virtual bool handle_external_ready_to_start_charging() override; diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index c2256164d..1c912d700 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 @@ -115,6 +119,24 @@ config: Enable self testing of IMD in cable check. This is required for IEC 61851-23 (2023) compliance. type: boolean default: true + cable_check_wait_below_60V_before_finish: + description: >- + Switch off power supply and wait until output voltage drops below 60V before cable check is finished. + Note: There are different versions of IEC 61851-23:2023 in the wild with the same version number but slightly different content. + The IEC was correcting mistakes _after_ releasing the document initially without tagging a new version number. + Some early versions require to wait for the output voltage to drop below 60V in CC.4.1.2 (last sentence). + Later versions do not have that requirement. The later versions are correct and should be used according to IEC. + Both settings (true and false) are compliant with the correct version of IEC 61851-23:2023. + Set to true when: + - the power supply has no active discharge, and lowering the voltage with no load takes a very long time. In this case + this option usually helps to ramp the voltage down quickly by switching it off. It will be switched on again in precharge. + Also, some EVs switch their internal relay on at a too high voltage when the voltage is ramped down directly from cablecheck voltage to precharge voltage, + so true is the recommended default. + Set to false when: + - the power supply has active discharge and can ramp down quickly without a switch off (by just setting a lower target voltage). + This may save a few seconds as it avoids unnecessary voltage down and up ramping. + type: boolean + default: true hack_skoda_enyaq: description: >- Skoda Enyaq requests DC charging voltages below its battery level or even below 0 initially. @@ -257,6 +279,18 @@ config: and this violates IEC61851-1:2019 D.6.5 Table D.9 line 4 and should not be used in public environments! type: boolean default: true + state_F_after_fault_ms: + description: >- + Set state F after any fault that stops charging for the specified time in ms while in Charging mode (CX->F(300ms)->C1/B1). + When a fault occurs in state B2, no state F is added (B2->B1 on fault). + Some (especially older hybrid vehicles) may go into a permanent fault mode once they detect state F, + in this case EVerest cannot recover the charging session if the fault is cleared. + In this case you can set this parameter to 0, which will avoid to use state F in case of a fault + and only disables PWM (C2->C1) while switching off power. This will violate IEC 61851-1:2017 however. + The default is 300ms as the minimum suggested by IEC 61851-1:2017 Table A.5 (description) to be compliant. + This setting is only active in BASIC charging mode. + type: integer + default: 300 provides: evse: interface: evse_manager diff --git a/modules/EvseManager/tests/CMakeLists.txt b/modules/EvseManager/tests/CMakeLists.txt index 33d060038..2bccddf3d 100644 --- a/modules/EvseManager/tests/CMakeLists.txt +++ b/modules/EvseManager/tests/CMakeLists.txt @@ -6,7 +6,7 @@ add_dependencies(${TEST_TARGET_NAME} ${MODULE_NAME}) get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) target_include_directories(${TEST_TARGET_NAME} PRIVATE - . .. ../../../tests/include + .. ../../../tests/include ${GENERATED_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} ) diff --git a/modules/EvseManager/tests/evse_board_supportIntfStub.hpp b/modules/EvseManager/tests/evse_board_supportIntfStub.hpp index 9ed3c0364..c64342848 100644 --- a/modules/EvseManager/tests/evse_board_supportIntfStub.hpp +++ b/modules/EvseManager/tests/evse_board_supportIntfStub.hpp @@ -12,7 +12,7 @@ namespace module::stub { struct evse_board_supportIntfStub : public evse_board_supportIntf { explicit evse_board_supportIntfStub(ModuleAdapterStub& adapter) : - evse_board_supportIntf(&adapter, Requirement("requirement", 1), "EvseManager") { + evse_board_supportIntf(&adapter, Requirement{"requirement", 1}, "EvseManager", std::nullopt) { } }; 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/EvseSecurity/main/evse_securityImpl.cpp b/modules/EvseSecurity/main/evse_securityImpl.cpp index 050782928..b18d9d9aa 100644 --- a/modules/EvseSecurity/main/evse_securityImpl.cpp +++ b/modules/EvseSecurity/main/evse_securityImpl.cpp @@ -121,10 +121,34 @@ evse_securityImpl::handle_get_leaf_certificate_info(types::evse_security::LeafCe return response; } +types::evse_security::GetCertificateFullInfoResult +evse_securityImpl::handle_get_all_valid_certificates_info(types::evse_security::LeafCertificateType& certificate_type, + types::evse_security::EncodingFormat& encoding, + bool& include_ocsp) { + types::evse_security::GetCertificateFullInfoResult response; + + const auto full_leaf_info = this->evse_security->get_all_valid_certificates_info( + conversions::from_everest(certificate_type), conversions::from_everest(encoding), include_ocsp); + + response.status = conversions::to_everest(full_leaf_info.status); + + if (full_leaf_info.status == evse_security::GetCertificateInfoStatus::Accepted) { + for (const auto& info : full_leaf_info.info) { + response.info.push_back(conversions::to_everest(info)); + } + } + + return response; +} + std::string evse_securityImpl::handle_get_verify_file(types::evse_security::CaCertificateType& certificate_type) { return this->evse_security->get_verify_file(conversions::from_everest(certificate_type)); } +std::string evse_securityImpl::handle_get_verify_location(types::evse_security::CaCertificateType& certificate_type) { + return this->evse_security->get_verify_location(conversions::from_everest(certificate_type)); +} + int evse_securityImpl::handle_get_leaf_expiry_days_count(types::evse_security::LeafCertificateType& certificate_type) { return this->evse_security->get_leaf_expiry_days_count(conversions::from_everest(certificate_type)); } diff --git a/modules/EvseSecurity/main/evse_securityImpl.hpp b/modules/EvseSecurity/main/evse_securityImpl.hpp index 0b278ae6f..26d20d79d 100644 --- a/modules/EvseSecurity/main/evse_securityImpl.hpp +++ b/modules/EvseSecurity/main/evse_securityImpl.hpp @@ -60,7 +60,11 @@ class evse_securityImpl : public evse_securityImplBase { virtual types::evse_security::GetCertificateInfoResult handle_get_leaf_certificate_info(types::evse_security::LeafCertificateType& certificate_type, types::evse_security::EncodingFormat& encoding, bool& include_ocsp) override; + virtual types::evse_security::GetCertificateFullInfoResult + handle_get_all_valid_certificates_info(types::evse_security::LeafCertificateType& certificate_type, + types::evse_security::EncodingFormat& encoding, bool& include_ocsp) override; virtual std::string handle_get_verify_file(types::evse_security::CaCertificateType& certificate_type) override; + virtual std::string handle_get_verify_location(types::evse_security::CaCertificateType& certificate_type) override; virtual int handle_get_leaf_expiry_days_count(types::evse_security::LeafCertificateType& certificate_type) override; virtual bool handle_verify_file_signature(std::string& file_path, std::string& signing_certificate, std::string& signature) override; diff --git a/modules/EvseV2G/CMakeLists.txt b/modules/EvseV2G/CMakeLists.txt index 37702aa7a..af18dc458 100644 --- a/modules/EvseV2G/CMakeLists.txt +++ b/modules/EvseV2G/CMakeLists.txt @@ -59,13 +59,14 @@ target_sources(${MODULE_NAME} if(USING_MBED_TLS) # needed for header file enum definition target_include_directories(${MODULE_NAME} PRIVATE - ../../lib/staging/tls ../../lib/staging/util + ../../lib/staging/tls ) target_link_libraries(${MODULE_NAME} PRIVATE mbedcrypto mbedtls mbedx509 + everest::staging::util ) target_sources(${MODULE_NAME} PRIVATE diff --git a/modules/EvseV2G/EvseV2G.cpp b/modules/EvseV2G/EvseV2G.cpp index 90dee57eb..be9d2f70e 100644 --- a/modules/EvseV2G/EvseV2G.cpp +++ b/modules/EvseV2G/EvseV2G.cpp @@ -16,6 +16,9 @@ void log_handler(openssl::log_level_t level, const std::string& str) { case openssl::log_level_t::debug: // ignore debug logs break; + case openssl::log_level_t::info: + EVLOG_info << str; + break; case openssl::log_level_t::warning: EVLOG_warning << str; break; @@ -60,11 +63,13 @@ void EvseV2G::ready() { goto err_out; } - rv = sdp_init(v2g_ctx); + if (config.enable_sdp_server) { + rv = sdp_init(v2g_ctx); - if (rv == -1) { - dlog(DLOG_LEVEL_ERROR, "Failed to start SDP responder"); - goto err_out; + if (rv == -1) { + dlog(DLOG_LEVEL_ERROR, "Failed to start SDP responder"); + goto err_out; + } } dlog(DLOG_LEVEL_DEBUG, "starting socket server(s)"); diff --git a/modules/EvseV2G/EvseV2G.hpp b/modules/EvseV2G/EvseV2G.hpp index 851dd5d6e..c1a497e0a 100644 --- a/modules/EvseV2G/EvseV2G.hpp +++ b/modules/EvseV2G/EvseV2G.hpp @@ -38,6 +38,7 @@ struct Conf { bool verify_contract_cert_chain; int auth_timeout_pnc; int auth_timeout_eim; + bool enable_sdp_server; }; class EvseV2G : public Everest::ModuleBase { diff --git a/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp b/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp index c7b8376ee..a1ca76375 100644 --- a/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp +++ b/modules/EvseV2G/charger/ISO15118_chargerImpl.cpp @@ -68,7 +68,7 @@ void ISO15118_chargerImpl::ready() { void ISO15118_chargerImpl::handle_setup( types::iso15118_charger::EVSEID& evse_id, - std::vector& supported_energy_transfer_modes, + std::vector& supported_energy_transfer_modes, types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { uint8_t len = evse_id.evse_id.length(); @@ -89,9 +89,14 @@ void ISO15118_chargerImpl::handle_setup( v2g_ctx->is_dc_charger = true; - for (auto& energy_transfer_mode : supported_energy_transfer_modes) { + for (const auto& mode : supported_energy_transfer_modes) { - switch (energy_transfer_mode) { + if (mode.bidirectional) { + dlog(DLOG_LEVEL_INFO, "Ignoring bidirectional SupportedEnergyTransferMode"); + continue; + } + + switch (mode.energy_transfer_mode) { case types::iso15118_charger::EnergyTransferMode::AC_single_phase_core: energyArray[(energyArrayLen)++] = iso2_EnergyTransferModeType_AC_single_phase_core; v2g_ctx->is_dc_charger = false; @@ -116,7 +121,7 @@ void ISO15118_chargerImpl::handle_setup( if (energyArrayLen == 0) { dlog(DLOG_LEVEL_WARNING, "Unable to configure SupportedEnergyTransferMode %s", - types::iso15118_charger::energy_transfer_mode_to_string(energy_transfer_mode).c_str()); + types::iso15118_charger::energy_transfer_mode_to_string(mode.energy_transfer_mode).c_str()); } break; } @@ -208,7 +213,7 @@ void ISO15118_chargerImpl::handle_session_setup(std::vectormqtt_lock); - if (exi_stream_status.exi_response.has_value()) { + if (exi_stream_status.exi_response.has_value() and not exi_stream_status.exi_response.value().empty()) { v2g_ctx->evse_v2g_data.cert_install_res_b64_buffer = std::string(exi_stream_status.exi_response.value()); } v2g_ctx->evse_v2g_data.cert_install_status = diff --git a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp index df011e544..f08233d73 100644 --- a/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp +++ b/modules/EvseV2G/charger/ISO15118_chargerImpl.hpp @@ -34,9 +34,10 @@ class ISO15118_chargerImpl : public ISO15118_chargerImplBase { protected: // command handler functions (virtual) - virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, - std::vector& supported_energy_transfer_modes, - types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; + virtual void + handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) override; virtual void handle_session_setup(std::vector& payment_options, bool& supported_certificate_service) override; diff --git a/modules/EvseV2G/connection/connection.cpp b/modules/EvseV2G/connection/connection.cpp index 59f423a57..a0ab42476 100644 --- a/modules/EvseV2G/connection/connection.cpp +++ b/modules/EvseV2G/connection/connection.cpp @@ -184,6 +184,9 @@ static int connection_ssl_initialize() { * \return Returns \c 0 on success, otherwise \c -1 */ int check_interface(struct v2g_context* v2g_ctx) { + if (v2g_ctx == nullptr || v2g_ctx->if_name == nullptr) { + return -1; + } struct ipv6_mreq mreq = {}; std::memset(&mreq, 0, sizeof(mreq)); @@ -192,6 +195,10 @@ int check_interface(struct v2g_context* v2g_ctx) { v2g_ctx->if_name = choose_first_ipv6_interface(); } + if (v2g_ctx->if_name == nullptr) { + return -1; + } + mreq.ipv6mr_interface = if_nametoindex(v2g_ctx->if_name); if (!mreq.ipv6mr_interface) { dlog(DLOG_LEVEL_ERROR, "No such interface: %s", v2g_ctx->if_name); diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp index 586613ad2..25d374c0a 100644 --- a/modules/EvseV2G/connection/tls_connection.cpp +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -3,7 +3,6 @@ #include "tls_connection.hpp" #include "connection.hpp" -#include "everest/logging.hpp" #include "log.hpp" #include "v2g.hpp" #include "v2g_server.hpp" @@ -140,32 +139,40 @@ bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { config.socket = ctx->tls_socket.fd; config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); + config.tls_key_logging = ctx->tls_key_logging; + config.tls_key_logging_path = ctx->tls_key_logging_path; + config.host = ctx->if_name; + // information from libevse-security const auto cert_info = - ctx->r_security->call_get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, false); + ctx->r_security->call_get_all_valid_certificates_info(LeafCertificateType::V2G, EncodingFormat::PEM, true); if (cert_info.status != GetCertificateInfoStatus::Accepted) { dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Not Accepted"); } else { - if (cert_info.info) { - const auto& info = cert_info.info.value(); - const auto cert_path = info.certificate.value_or(""); - const auto key_path = info.key; - - // workaround (see above libevse-security comment) - const auto key_password = info.password.value_or(""); - - auto& ref = config.chains.emplace_back(); - ref.certificate_chain_file = cert_path.c_str(); - ref.private_key_file = key_path.c_str(); - ref.private_key_password = key_password.c_str(); - - if (info.ocsp) { - for (const auto& ocsp : info.ocsp.value()) { - const char* file{nullptr}; - if (ocsp.ocsp_path) { - file = ocsp.ocsp_path.value().c_str(); + if (!cert_info.info.empty()) { + // process all known certificate chains + for (const auto& chain : cert_info.info) { + const auto cert_path = chain.certificate.value_or(""); + const auto key_path = chain.key; + const auto root_pem = chain.certificate_root.value_or(""); + + // workaround (see above libevse-security comment) + const auto key_password = chain.password.value_or(""); + + auto& ref = config.chains.emplace_back(); + ref.certificate_chain_file = cert_path.c_str(); + ref.private_key_file = key_path.c_str(); + ref.private_key_password = key_password.c_str(); + ref.trust_anchor_pem = root_pem.c_str(); + + if (chain.ocsp) { + for (const auto& ocsp : chain.ocsp.value()) { + const char* file{nullptr}; + if (ocsp.ocsp_path) { + file = ocsp.ocsp_path.value().c_str(); + } + ref.ocsp_response_files.push_back(file); } - ref.ocsp_response_files.push_back(file); } } diff --git a/modules/EvseV2G/iso_server.cpp b/modules/EvseV2G/iso_server.cpp index 5cdf04279..21babf8b4 100644 --- a/modules/EvseV2G/iso_server.cpp +++ b/modules/EvseV2G/iso_server.cpp @@ -956,7 +956,6 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { break; case crypto::verify_result_t::NoCertificateAvailable: res->ResponseCode = iso2_responseCodeType_FAILED_NoCertificateAvailable; - err = -2; break; case crypto::verify_result_t::CertChainError: default: @@ -970,10 +969,6 @@ static enum v2g_event handle_iso_payment_details(struct v2g_connection* conn) { res->EVSETimeStamp = time(NULL); memset(res->GenChallenge.bytes, 0, GEN_CHALLENGE_SIZE); res->GenChallenge.bytesLen = GEN_CHALLENGE_SIZE; - } - - if (err != 0) { - memset(res, 0, sizeof(*res)); goto error_out; } @@ -1495,9 +1490,11 @@ static enum v2g_event handle_iso_charging_status(struct v2g_connection* conn) { /* build up response */ res->ResponseCode = iso2_responseCodeType_OK; - res->ReceiptRequired = conn->ctx->evse_v2g_data.receipt_required; - res->ReceiptRequired_isUsed = - (conn->ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract) ? 1U : 0U; + res->ReceiptRequired = false; // [V2G2-691] ReceiptRequired shall be false in case of EIM + if (conn->ctx->session.iso_selected_payment_option == iso2_paymentOptionType_Contract) { + res->ReceiptRequired = conn->ctx->evse_v2g_data.receipt_required; + } + res->ReceiptRequired_isUsed = true; // [V2G2-691] ChargingStatusRes shall always include ReceiptRequired if (conn->ctx->meter_info.meter_info_is_used == true) { res->MeterInfo.MeterID.charactersLen = conn->ctx->meter_info.meter_id.bytesLen; @@ -2034,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/EvseV2G/manifest.yaml b/modules/EvseV2G/manifest.yaml index c7623fb60..cf842ea54 100644 --- a/modules/EvseV2G/manifest.yaml +++ b/modules/EvseV2G/manifest.yaml @@ -70,6 +70,11 @@ config: Write 0 if the EVSE should wait indefinitely for EIM authorization. type: integer default: 300 + enable_sdp_server: + description: >- + Enable the built-in SDP server + type: boolean + default: true provides: charger: interface: ISO15118_charger diff --git a/modules/EvseV2G/tests/CMakeLists.txt b/modules/EvseV2G/tests/CMakeLists.txt index 5b5adf32b..82f81f20b 100644 --- a/modules/EvseV2G/tests/CMakeLists.txt +++ b/modules/EvseV2G/tests/CMakeLists.txt @@ -27,7 +27,7 @@ add_dependencies(${TLS_GTEST_NAME} v2g_test_files_target) add_dependencies(${TLS_GTEST_NAME} generate_cpp_files) target_include_directories(${TLS_GTEST_NAME} PRIVATE - . .. ../crypto ../../../lib/staging/util + .. ../crypto ${GENERATED_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} ) @@ -59,7 +59,7 @@ add_executable(${V2G_MAIN_NAME}) add_dependencies(${V2G_MAIN_NAME} generate_cpp_files) target_include_directories(${V2G_MAIN_NAME} PRIVATE - . .. ../connection ../../../tests/include ../../../lib/staging/util + .. ../connection ../../../tests/include ${GENERATED_INCLUDE_DIR} ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} ${CMAKE_BINARY_DIR}/generated/include diff --git a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp index a6779623f..d3a33f315 100644 --- a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp +++ b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp @@ -20,9 +20,10 @@ struct ISO15118_chargerImplStub : public ISO15118_chargerImplBase { virtual void ready() { } - virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, - std::vector& supported_energy_transfer_modes, - types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { + virtual void + handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { std::cout << "ISO15118_chargerImplBase::handle_setup called" << std::endl; } virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) { diff --git a/modules/EvseV2G/tests/evse_securityIntfStub.hpp b/modules/EvseV2G/tests/evse_securityIntfStub.hpp index d2a388e0a..5768457c6 100644 --- a/modules/EvseV2G/tests/evse_securityIntfStub.hpp +++ b/modules/EvseV2G/tests/evse_securityIntfStub.hpp @@ -23,7 +23,7 @@ class evse_securityIntfStub : public ModuleAdapterStub, public evse_securityIntf functions; public: - evse_securityIntfStub() : evse_securityIntf(this, Requirement("", 0), "EvseSecurity") { + evse_securityIntfStub() : evse_securityIntf(this, Requirement{"", 0}, "EvseSecurity", std::nullopt) { functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file; functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info; } diff --git a/modules/EvseV2G/tests/requirement.cpp b/modules/EvseV2G/tests/requirement.cpp index cec9c44a7..8bbd5db72 100644 --- a/modules/EvseV2G/tests/requirement.cpp +++ b/modules/EvseV2G/tests/requirement.cpp @@ -5,8 +5,6 @@ #include "utils/types.hpp" -Requirement::Requirement(const std::string& requirement_id_, size_t index_) { -} -bool Requirement::operator<(const Requirement& rhs) const { +bool operator<(const Requirement& lhs, const Requirement& rhs) { return true; } diff --git a/modules/EvseV2G/tools.cpp b/modules/EvseV2G/tools.cpp index f94db476d..85e9a887c 100644 --- a/modules/EvseV2G/tools.cpp +++ b/modules/EvseV2G/tools.cpp @@ -90,6 +90,12 @@ int get_interface_ipv6_address(const char* if_name, enum Addr6Type type, struct struct ifaddrs *ifaddr, *ifa; int rv = -1; + // If using loopback device, accept any address + // (lo usually does not have a link local address) + if (strcmp(if_name, "lo") == 0) { + type = ADDR6_TYPE_UNPSEC; + } + if (getifaddrs(&ifaddr) == -1) return -1; diff --git a/modules/EvseV2G/v2g_server.cpp b/modules/EvseV2G/v2g_server.cpp index 4cb886327..891d147dc 100644 --- a/modules/EvseV2G/v2g_server.cpp +++ b/modules/EvseV2G/v2g_server.cpp @@ -144,9 +144,9 @@ static void publish_var_V2G_Message(v2g_connection* conn, bool is_req) { } #endif // EVEREST_MBED_TLS - v2g_message.v2g_message_exi_base64 = EXI_Base64; - v2g_message.v2g_message_id = get_v2g_message_id(conn->ctx->current_v2g_msg, conn->ctx->selected_protocol, is_req); - v2g_message.v2g_message_exi_hex = msg_as_hex_string; + v2g_message.exi_base64 = EXI_Base64; + v2g_message.id = get_v2g_message_id(conn->ctx->current_v2g_msg, conn->ctx->selected_protocol, is_req); + v2g_message.exi = msg_as_hex_string; conn->ctx->p_charger->publish_v2g_messages(v2g_message); } diff --git a/modules/IsoMux/CMakeLists.txt b/modules/IsoMux/CMakeLists.txt new file mode 100644 index 000000000..3bae2eb4b --- /dev/null +++ b/modules/IsoMux/CMakeLists.txt @@ -0,0 +1,55 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +# insert your custom targets and additional config variables here +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "charger/ISO15118_chargerImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# Add pkg-config functionality +find_package(PkgConfig REQUIRED) + +target_include_directories(${MODULE_NAME} PRIVATE + crypto + connection +) + +target_link_libraries(${MODULE_NAME} PUBLIC -lpthread) + +target_link_libraries(${MODULE_NAME} + PRIVATE + cbv2g::din + cbv2g::iso2 + cbv2g::tp +) + +target_sources(${MODULE_NAME} + PRIVATE + "connection/connection.cpp" + "log.cpp" + "sdp.cpp" + "tools.cpp" + "v2g_ctx.cpp" + "v2g_server.cpp" +) + +target_link_libraries(${MODULE_NAME} + PRIVATE + everest::tls +) +target_sources(${MODULE_NAME} + PRIVATE + "connection/tls_connection.cpp" +) +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/IsoMux/IsoMux.cpp b/modules/IsoMux/IsoMux.cpp new file mode 100644 index 000000000..f9a4669e7 --- /dev/null +++ b/modules/IsoMux/IsoMux.cpp @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#include "IsoMux.hpp" +#include "connection.hpp" +#include "log.hpp" +#include "sdp.hpp" + +#include +namespace { +void log_handler(openssl::log_level_t level, const std::string& str) { + switch (level) { + case openssl::log_level_t::debug: + // ignore debug logs + break; + case openssl::log_level_t::info: + EVLOG_info << str; + break; + case openssl::log_level_t::warning: + EVLOG_warning << str; + break; + case openssl::log_level_t::error: + default: + EVLOG_error << str; + break; + } +} +} // namespace + +struct v2g_context* v2g_ctx = nullptr; + +namespace module { + +void IsoMux::init() { + /* create v2g context */ + v2g_ctx = v2g_ctx_create(&(*r_security)); + + if (v2g_ctx == nullptr) + return; + + v2g_ctx->proxy_port_iso2 = config.proxy_port_iso2; + v2g_ctx->proxy_port_iso20 = config.proxy_port_iso20; + v2g_ctx->selected_iso20 = false; + + v2g_ctx->tls_key_logging = config.tls_key_logging; + + (void)openssl::set_log_handler(log_handler); + v2g_ctx->tls_server = &tls_server; + + invoke_init(*p_charger); +} + +void IsoMux::ready() { + int rv = 0; + + dlog(DLOG_LEVEL_DEBUG, "Starting SDP responder"); + + rv = connection_init(v2g_ctx); + + if (rv == -1) { + dlog(DLOG_LEVEL_ERROR, "Failed to initialize connection"); + goto err_out; + } + + rv = sdp_init(v2g_ctx); + + if (rv == -1) { + dlog(DLOG_LEVEL_ERROR, "Failed to start SDP responder"); + goto err_out; + } + + dlog(DLOG_LEVEL_DEBUG, "starting socket server(s)"); + if (connection_start_servers(v2g_ctx)) { + dlog(DLOG_LEVEL_ERROR, "start_connection_servers() failed"); + goto err_out; + } + + invoke_ready(*p_charger); + + rv = sdp_listen(v2g_ctx); + + if (rv == -1) { + dlog(DLOG_LEVEL_ERROR, "sdp_listen() failed"); + goto err_out; + } + + return; + +err_out: + v2g_ctx_free(v2g_ctx); +} + +IsoMux::~IsoMux() { + v2g_ctx_free(v2g_ctx); +} + +bool IsoMux::selected_iso20() { + return v2g_ctx->selected_iso20; +} + +} // namespace module diff --git a/modules/IsoMux/IsoMux.hpp b/modules/IsoMux/IsoMux.hpp new file mode 100644 index 000000000..02acb8fd0 --- /dev/null +++ b/modules/IsoMux/IsoMux.hpp @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef ISO_MUX_HPP +#define ISO_MUX_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// headers for required interface implementations +#include +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +#include "v2g_ctx.hpp" +#include +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + std::string device; + std::string tls_security; + bool tls_key_logging; + int tls_timeout; + int proxy_port_iso2; + int proxy_port_iso20; +}; + +class IsoMux : public Everest::ModuleBase { +public: + IsoMux() = delete; + IsoMux(const ModuleInfo& info, std::unique_ptr p_charger, + std::unique_ptr r_security, std::unique_ptr r_iso2, + std::unique_ptr r_iso20, Conf& config) : + ModuleBase(info), + p_charger(std::move(p_charger)), + r_security(std::move(r_security)), + r_iso2(std::move(r_iso2)), + r_iso20(std::move(r_iso20)), + config(config){}; + + const std::unique_ptr p_charger; + const std::unique_ptr r_security; + const std::unique_ptr r_iso2; + const std::unique_ptr r_iso20; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + ~IsoMux(); + bool selected_iso20(); + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + + tls::Server tls_server; + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // ISO_MUX_HPP diff --git a/modules/IsoMux/charger/ISO15118_chargerImpl.cpp b/modules/IsoMux/charger/ISO15118_chargerImpl.cpp new file mode 100644 index 000000000..53a045b7c --- /dev/null +++ b/modules/IsoMux/charger/ISO15118_chargerImpl.cpp @@ -0,0 +1,602 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#include "ISO15118_chargerImpl.hpp" +#include "log.hpp" +#include "v2g_ctx.hpp" + +const std::string CERTS_SUB_DIR = "certs"; // relativ path of the certs + +using namespace std::chrono_literals; +using BidiMode = types::iso15118_charger::SaeJ2847BidiMode; + +namespace module { +namespace charger { + +void ISO15118_chargerImpl::init() { + if (!v2g_ctx) { + dlog(DLOG_LEVEL_ERROR, "v2g_ctx not created"); + return; + } + + /* Configure if_name and auth_mode */ + v2g_ctx->if_name = mod->config.device.data(); + dlog(DLOG_LEVEL_DEBUG, "if_name %s", v2g_ctx->if_name); + + /* Configure tls_security */ + if (mod->config.tls_security == "force") { + v2g_ctx->tls_security = TLS_SECURITY_FORCE; + dlog(DLOG_LEVEL_DEBUG, "tls_security force"); + } else if (mod->config.tls_security == "allow") { + v2g_ctx->tls_security = TLS_SECURITY_ALLOW; + dlog(DLOG_LEVEL_DEBUG, "tls_security allow"); + } else { + v2g_ctx->tls_security = TLS_SECURITY_PROHIBIT; + dlog(DLOG_LEVEL_DEBUG, "tls_security prohibit"); + } + + v2g_ctx->network_read_timeout_tls = mod->config.tls_timeout; + + v2g_ctx->certs_path = mod->info.paths.etc / CERTS_SUB_DIR; + + // Subscribe all vars + mod->r_iso2->subscribe_require_auth_eim([this]() { + if (not mod->selected_iso20()) { + publish_require_auth_eim(nullptr); + } + }); + mod->r_iso20->subscribe_require_auth_eim([this]() { + if (mod->selected_iso20()) { + publish_require_auth_eim(nullptr); + } + }); + + mod->r_iso2->subscribe_require_auth_pnc([this](const auto value) { + if (not mod->selected_iso20()) { + publish_require_auth_pnc(value); + } + }); + mod->r_iso20->subscribe_require_auth_pnc([this](const auto value) { + if (mod->selected_iso20()) { + publish_require_auth_pnc(value); + } + }); + + mod->r_iso2->subscribe_ac_close_contactor([this]() { + if (not mod->selected_iso20()) { + publish_ac_close_contactor(nullptr); + } + }); + mod->r_iso20->subscribe_ac_close_contactor([this]() { + if (mod->selected_iso20()) { + publish_ac_close_contactor(nullptr); + } + }); + + mod->r_iso2->subscribe_ac_open_contactor([this]() { + if (not mod->selected_iso20()) { + publish_ac_open_contactor(nullptr); + } + }); + mod->r_iso20->subscribe_ac_open_contactor([this]() { + if (mod->selected_iso20()) { + publish_ac_open_contactor(nullptr); + } + }); + + mod->r_iso2->subscribe_start_cable_check([this]() { + if (not mod->selected_iso20()) { + publish_start_cable_check(nullptr); + } + }); + mod->r_iso20->subscribe_start_cable_check([this]() { + if (mod->selected_iso20()) { + publish_start_cable_check(nullptr); + } + }); + + mod->r_iso2->subscribe_dc_open_contactor([this]() { + if (not mod->selected_iso20()) { + publish_dc_open_contactor(nullptr); + } + }); + mod->r_iso20->subscribe_dc_open_contactor([this]() { + if (mod->selected_iso20()) { + publish_dc_open_contactor(nullptr); + } + }); + + mod->r_iso2->subscribe_v2g_setup_finished([this]() { + if (not mod->selected_iso20()) { + publish_v2g_setup_finished(nullptr); + } + }); + mod->r_iso20->subscribe_v2g_setup_finished([this]() { + if (mod->selected_iso20()) { + publish_v2g_setup_finished(nullptr); + } + }); + + mod->r_iso2->subscribe_current_demand_started([this]() { + if (not mod->selected_iso20()) { + publish_current_demand_started(nullptr); + } + }); + mod->r_iso20->subscribe_current_demand_started([this]() { + if (mod->selected_iso20()) { + publish_current_demand_started(nullptr); + } + }); + + mod->r_iso2->subscribe_current_demand_finished([this]() { + if (not mod->selected_iso20()) { + publish_current_demand_finished(nullptr); + } + }); + mod->r_iso20->subscribe_current_demand_finished([this]() { + if (mod->selected_iso20()) { + publish_current_demand_finished(nullptr); + } + }); + + mod->r_iso2->subscribe_sae_bidi_mode_active([this]() { + if (not mod->selected_iso20()) { + publish_sae_bidi_mode_active(nullptr); + } + }); + mod->r_iso20->subscribe_sae_bidi_mode_active([this]() { + if (mod->selected_iso20()) { + publish_sae_bidi_mode_active(nullptr); + } + }); + + mod->r_iso2->subscribe_evcc_id([this](const auto id) { + if (not mod->selected_iso20()) { + publish_evcc_id(id); + } + }); + mod->r_iso20->subscribe_evcc_id([this](const auto id) { + if (mod->selected_iso20()) { + publish_evcc_id(id); + } + }); + + mod->r_iso2->subscribe_selected_payment_option([this](const auto o) { + if (not mod->selected_iso20()) { + publish_selected_payment_option(o); + } + }); + mod->r_iso20->subscribe_selected_payment_option([this](const auto o) { + if (mod->selected_iso20()) { + publish_selected_payment_option(o); + } + }); + + mod->r_iso2->subscribe_requested_energy_transfer_mode([this](const auto o) { + if (not mod->selected_iso20()) { + publish_requested_energy_transfer_mode(o); + } + }); + mod->r_iso20->subscribe_requested_energy_transfer_mode([this](const auto o) { + if (mod->selected_iso20()) { + publish_requested_energy_transfer_mode(o); + } + }); + + mod->r_iso2->subscribe_departure_time([this](const auto o) { + if (not mod->selected_iso20()) { + publish_departure_time(o); + } + }); + mod->r_iso20->subscribe_departure_time([this](const auto o) { + if (mod->selected_iso20()) { + publish_departure_time(o); + } + }); + + mod->r_iso2->subscribe_ac_eamount([this](const auto o) { + if (not mod->selected_iso20()) { + publish_ac_eamount(o); + } + }); + mod->r_iso20->subscribe_ac_eamount([this](const auto o) { + if (mod->selected_iso20()) { + publish_ac_eamount(o); + } + }); + + mod->r_iso2->subscribe_ac_ev_max_voltage([this](const auto o) { + if (not mod->selected_iso20()) { + publish_ac_ev_max_voltage(o); + } + }); + mod->r_iso20->subscribe_ac_ev_max_voltage([this](const auto o) { + if (mod->selected_iso20()) { + publish_ac_ev_max_voltage(o); + } + }); + + mod->r_iso2->subscribe_ac_ev_max_current([this](const auto o) { + if (not mod->selected_iso20()) { + publish_ac_ev_max_current(o); + } + }); + mod->r_iso20->subscribe_ac_ev_max_current([this](const auto o) { + if (mod->selected_iso20()) { + publish_ac_ev_max_current(o); + } + }); + + mod->r_iso2->subscribe_ac_ev_min_current([this](const auto o) { + if (not mod->selected_iso20()) { + publish_ac_ev_min_current(o); + } + }); + mod->r_iso20->subscribe_ac_ev_min_current([this](const auto o) { + if (mod->selected_iso20()) { + publish_ac_ev_min_current(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_energy_capacity([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_energy_capacity(o); + } + }); + mod->r_iso20->subscribe_dc_ev_energy_capacity([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_energy_capacity(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_energy_request([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_energy_request(o); + } + }); + mod->r_iso20->subscribe_dc_ev_energy_request([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_energy_request(o); + } + }); + + mod->r_iso2->subscribe_dc_full_soc([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_full_soc(o); + } + }); + mod->r_iso20->subscribe_dc_full_soc([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_full_soc(o); + } + }); + + mod->r_iso2->subscribe_dc_bulk_soc([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_bulk_soc(o); + } + }); + mod->r_iso20->subscribe_dc_bulk_soc([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_bulk_soc(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_status([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_status(o); + } + }); + mod->r_iso20->subscribe_dc_ev_status([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_status(o); + } + }); + + mod->r_iso2->subscribe_dc_bulk_charging_complete([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_bulk_charging_complete(o); + } + }); + mod->r_iso20->subscribe_dc_bulk_charging_complete([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_bulk_charging_complete(o); + } + }); + + mod->r_iso2->subscribe_dc_charging_complete([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_charging_complete(o); + } + }); + mod->r_iso20->subscribe_dc_charging_complete([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_charging_complete(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_target_voltage_current([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_target_voltage_current(o); + } + }); + mod->r_iso20->subscribe_dc_ev_target_voltage_current([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_target_voltage_current(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_maximum_limits([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_maximum_limits(o); + } + }); + mod->r_iso20->subscribe_dc_ev_maximum_limits([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_maximum_limits(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_remaining_time([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_remaining_time(o); + } + }); + mod->r_iso20->subscribe_dc_ev_remaining_time([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_remaining_time(o); + } + }); + + mod->r_iso2->subscribe_certificate_request([this](const auto o) { + if (not mod->selected_iso20()) { + publish_certificate_request(o); + } + }); + mod->r_iso20->subscribe_certificate_request([this](const auto o) { + if (mod->selected_iso20()) { + publish_certificate_request(o); + } + }); + + mod->r_iso2->subscribe_dlink_terminate([this]() { + if (not mod->selected_iso20()) { + publish_dlink_terminate(nullptr); + } + }); + mod->r_iso20->subscribe_dlink_terminate([this]() { + if (mod->selected_iso20()) { + publish_dlink_terminate(nullptr); + } + }); + + mod->r_iso2->subscribe_dlink_error([this]() { + if (not mod->selected_iso20()) { + publish_dlink_error(nullptr); + } + }); + mod->r_iso20->subscribe_dlink_error([this]() { + if (mod->selected_iso20()) { + publish_dlink_error(nullptr); + } + }); + + mod->r_iso2->subscribe_dlink_pause([this]() { + if (not mod->selected_iso20()) { + publish_dlink_pause(nullptr); + } + }); + mod->r_iso20->subscribe_dlink_pause([this]() { + if (mod->selected_iso20()) { + publish_dlink_pause(nullptr); + } + }); + + mod->r_iso2->subscribe_ev_app_protocol([this](const auto o) { + if (not mod->selected_iso20()) { + publish_ev_app_protocol(o); + } + }); + mod->r_iso20->subscribe_ev_app_protocol([this](const auto o) { + if (mod->selected_iso20()) { + publish_ev_app_protocol(o); + } + }); + + mod->r_iso2->subscribe_v2g_messages([this](const auto o) { + if (not mod->selected_iso20()) { + publish_v2g_messages(o); + } + }); + mod->r_iso20->subscribe_v2g_messages([this](const auto o) { + if (mod->selected_iso20()) { + publish_v2g_messages(o); + } + }); + + mod->r_iso2->subscribe_selected_protocol([this](const auto o) { + if (not mod->selected_iso20()) { + publish_selected_protocol(o); + } + }); + mod->r_iso20->subscribe_selected_protocol([this](const auto o) { + if (mod->selected_iso20()) { + publish_selected_protocol(o); + } + }); + + mod->r_iso2->subscribe_display_parameters([this](const auto o) { + if (not mod->selected_iso20()) { + publish_display_parameters(o); + } + }); + mod->r_iso20->subscribe_display_parameters([this](const auto o) { + if (mod->selected_iso20()) { + publish_display_parameters(o); + } + }); + + mod->r_iso20->subscribe_d20_dc_dynamic_charge_mode([this](const auto o) { + if (mod->selected_iso20()) { + publish_d20_dc_dynamic_charge_mode(o); + } + }); + + mod->r_iso2->subscribe_dc_ev_present_voltage([this](const auto o) { + if (not mod->selected_iso20()) { + publish_dc_ev_present_voltage(o); + } + }); + + mod->r_iso20->subscribe_dc_ev_present_voltage([this](const auto o) { + if (mod->selected_iso20()) { + publish_dc_ev_present_voltage(o); + } + }); + + mod->r_iso2->subscribe_meter_info_requested([this]() { + if (not mod->selected_iso20()) { + publish_meter_info_requested(nullptr); + } + }); + + mod->r_iso20->subscribe_meter_info_requested([this]() { + if (mod->selected_iso20()) { + publish_meter_info_requested(nullptr); + } + }); +} + +void ISO15118_chargerImpl::ready() { +} + +void ISO15118_chargerImpl::handle_setup( + types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) { + mod->r_iso20->call_setup(evse_id, supported_energy_transfer_modes, sae_j2847_mode, debug_mode); + mod->r_iso2->call_setup(evse_id, supported_energy_transfer_modes, sae_j2847_mode, debug_mode); +} + +void ISO15118_chargerImpl::handle_set_charging_parameters( + types::iso15118_charger::SetupPhysicalValues& physical_values) { + mod->r_iso20->call_set_charging_parameters(physical_values); + mod->r_iso2->call_set_charging_parameters(physical_values); +} + +void ISO15118_chargerImpl::handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) { + mod->r_iso20->call_session_setup(payment_options, supported_certificate_service); + mod->r_iso2->call_session_setup(payment_options, supported_certificate_service); +} + +void ISO15118_chargerImpl::handle_certificate_response( + types::iso15118_charger::ResponseExiStreamStatus& exi_stream_status) { + if (mod->selected_iso20()) { + mod->r_iso20->call_certificate_response(exi_stream_status); + } else { + mod->r_iso2->call_certificate_response(exi_stream_status); + } +} + +void ISO15118_chargerImpl::handle_authorization_response( + types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) { + if (mod->selected_iso20()) { + mod->r_iso20->call_authorization_response(authorization_status, certificate_status); + } else { + mod->r_iso2->call_authorization_response(authorization_status, certificate_status); + } +} + +void ISO15118_chargerImpl::handle_ac_contactor_closed(bool& status) { + if (mod->selected_iso20()) { + mod->r_iso20->call_ac_contactor_closed(status); + } else { + mod->r_iso2->call_ac_contactor_closed(status); + } +} + +void ISO15118_chargerImpl::handle_dlink_ready(bool& value) { + if (mod->selected_iso20()) { + mod->r_iso20->call_dlink_ready(value); + } else { + mod->r_iso2->call_dlink_ready(value); + } +} + +void ISO15118_chargerImpl::handle_cable_check_finished(bool& status) { + if (mod->selected_iso20()) { + mod->r_iso20->call_cable_check_finished(status); + } else { + mod->r_iso2->call_cable_check_finished(status); + } +} + +void ISO15118_chargerImpl::handle_receipt_is_required(bool& receipt_required) { + mod->r_iso20->call_receipt_is_required(receipt_required); + mod->r_iso2->call_receipt_is_required(receipt_required); +} + +void ISO15118_chargerImpl::handle_stop_charging(bool& stop) { + if (mod->selected_iso20()) { + mod->r_iso20->call_stop_charging(stop); + } else { + mod->r_iso2->call_stop_charging(stop); + } +} + +void ISO15118_chargerImpl::handle_update_ac_max_current(double& max_current) { + mod->r_iso20->call_update_ac_max_current(max_current); + mod->r_iso2->call_update_ac_max_current(max_current); +} + +void ISO15118_chargerImpl::handle_update_dc_maximum_limits( + types::iso15118_charger::DcEvseMaximumLimits& maximum_limits) { + mod->r_iso20->call_update_dc_maximum_limits(maximum_limits); + mod->r_iso2->call_update_dc_maximum_limits(maximum_limits); +} + +void ISO15118_chargerImpl::handle_update_dc_minimum_limits( + types::iso15118_charger::DcEvseMinimumLimits& minimum_limits) { + mod->r_iso20->call_update_dc_minimum_limits(minimum_limits); + mod->r_iso2->call_update_dc_minimum_limits(minimum_limits); +} + +void ISO15118_chargerImpl::handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) { + if (mod->selected_iso20()) { + mod->r_iso20->call_update_isolation_status(isolation_status); + } else { + mod->r_iso2->call_update_isolation_status(isolation_status); + } +} + +void ISO15118_chargerImpl::handle_update_dc_present_values( + types::iso15118_charger::DcEvsePresentVoltageCurrent& present_voltage_current) { + mod->r_iso20->call_update_dc_present_values(present_voltage_current); + mod->r_iso2->call_update_dc_present_values(present_voltage_current); +} + +void ISO15118_chargerImpl::handle_update_meter_info(types::powermeter::Powermeter& powermeter) { + mod->r_iso20->call_update_meter_info(powermeter); + mod->r_iso2->call_update_meter_info(powermeter); +} + +void ISO15118_chargerImpl::handle_send_error(types::iso15118_charger::EvseError& error) { + if (mod->selected_iso20()) { + mod->r_iso20->call_send_error(error); + } else { + mod->r_iso2->call_send_error(error); + } +} + +void ISO15118_chargerImpl::handle_reset_error() { + if (mod->selected_iso20()) { + mod->r_iso20->call_reset_error(); + } else { + mod->r_iso2->call_reset_error(); + } +} + +} // namespace charger +} // namespace module diff --git a/modules/IsoMux/charger/ISO15118_chargerImpl.hpp b/modules/IsoMux/charger/ISO15118_chargerImpl.hpp new file mode 100644 index 000000000..a512ed491 --- /dev/null +++ b/modules/IsoMux/charger/ISO15118_chargerImpl.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef CHARGER_ISO15118_CHARGER_IMPL_HPP +#define CHARGER_ISO15118_CHARGER_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "IsoMux.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +#include "v2g.hpp" +extern struct v2g_context* v2g_ctx; +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace charger { + +struct Conf {}; + +class ISO15118_chargerImpl : public ISO15118_chargerImplBase { +public: + ISO15118_chargerImpl() = delete; + ISO15118_chargerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + ISO15118_chargerImplBase(ev, "charger"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void + handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SaeJ2847BidiMode& sae_j2847_mode, bool& debug_mode) override; + virtual void handle_set_charging_parameters(types::iso15118_charger::SetupPhysicalValues& physical_values) override; + virtual void handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) override; + virtual void + handle_certificate_response(types::iso15118_charger::ResponseExiStreamStatus& exi_stream_status) override; + virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) override; + virtual void handle_ac_contactor_closed(bool& status) override; + virtual void handle_dlink_ready(bool& value) override; + virtual void handle_cable_check_finished(bool& status) override; + virtual void handle_receipt_is_required(bool& receipt_required) override; + virtual void handle_stop_charging(bool& stop) override; + virtual void handle_update_ac_max_current(double& max_current) override; + virtual void handle_update_dc_maximum_limits(types::iso15118_charger::DcEvseMaximumLimits& maximum_limits) override; + virtual void handle_update_dc_minimum_limits(types::iso15118_charger::DcEvseMinimumLimits& minimum_limits) override; + virtual void handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) override; + virtual void handle_update_dc_present_values( + types::iso15118_charger::DcEvsePresentVoltageCurrent& present_voltage_current) override; + virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) override; + virtual void handle_send_error(types::iso15118_charger::EvseError& error) override; + virtual void handle_reset_error() override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace charger +} // namespace module + +#endif // CHARGER_ISO15118_CHARGER_IMPL_HPP diff --git a/modules/IsoMux/connection/connection.cpp b/modules/IsoMux/connection/connection.cpp new file mode 100644 index 000000000..4d3914fac --- /dev/null +++ b/modules/IsoMux/connection/connection.cpp @@ -0,0 +1,659 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest + +#include "connection.hpp" +#include "log.hpp" +#include "tls_connection.hpp" +#include "tools.hpp" +#include "v2g_server.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "proxy.hpp" + +#define DEFAULT_SOCKET_BACKLOG 3 +#define DEFAULT_TCP_PORT 61342 +#define DEFAULT_TLS_PORT 64110 +#define ERROR_SESSION_ALREADY_STARTED 2 + +/*! + * \brief connection_create_socket This function creates a tcp/tls socket + * \param sockaddr to bind the socket to an interface + * \return Returns \c 0 on success, otherwise \c -1 + */ +static int connection_create_socket(struct sockaddr_in6* sockaddr) { + socklen_t addrlen = sizeof(*sockaddr); + int s, enable = 1; + static bool error_once = false; + + /* create socket */ + s = socket(AF_INET6, SOCK_STREAM, 0); + if (s == -1) { + if (!error_once) { + dlog(DLOG_LEVEL_ERROR, "socket() failed: %s", strerror(errno)); + error_once = true; + } + return -1; + } + + if (setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable)) == -1) { + if (!error_once) { + dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_REUSEPORT) failed: %s", strerror(errno)); + error_once = true; + } + close(s); + return -1; + } + + /* bind it to interface */ + if (bind(s, reinterpret_cast(sockaddr), addrlen) == -1) { + if (!error_once) { + dlog(DLOG_LEVEL_WARNING, "bind() failed: %s", strerror(errno)); + error_once = true; + } + close(s); + return -1; + } + + /* listen on this socket */ + if (listen(s, DEFAULT_SOCKET_BACKLOG) == -1) { + if (!error_once) { + dlog(DLOG_LEVEL_ERROR, "listen() failed: %s", strerror(errno)); + error_once = true; + } + close(s); + return -1; + } + + /* retrieve the actual port number we are listening on */ + if (getsockname(s, reinterpret_cast(sockaddr), &addrlen) == -1) { + if (!error_once) { + dlog(DLOG_LEVEL_ERROR, "getsockname() failed: %s", strerror(errno)); + error_once = true; + } + close(s); + return -1; + } + + return s; +} + +/*! + * \brief check_interface This function checks the interface name. The interface name is + * configured automatically in case it is pre-initialized to “auto. + * \param sockaddr to bind the socket to an interface + * \return Returns \c 0 on success, otherwise \c -1 + */ +int check_interface(struct v2g_context* v2g_ctx) { + if (v2g_ctx == nullptr || v2g_ctx->if_name == nullptr) { + return -1; + } + + struct ipv6_mreq mreq = {}; + std::memset(&mreq, 0, sizeof(mreq)); + + if (strcmp(v2g_ctx->if_name, "auto") == 0) { + v2g_ctx->if_name = choose_first_ipv6_interface(); + } + + if (v2g_ctx->if_name == nullptr) { + return -1; + } + + mreq.ipv6mr_interface = if_nametoindex(v2g_ctx->if_name); + if (!mreq.ipv6mr_interface) { + dlog(DLOG_LEVEL_ERROR, "No such interface: %s", v2g_ctx->if_name); + return -1; + } + + return (v2g_ctx->if_name == nullptr) ? -1 : 0; +} + +/*! + * \brief connection_init This function initilizes the tcp and tls interface. + * \param v2g_context is the V2G context. + * \return Returns \c 0 on success, otherwise \c -1 + */ +int connection_init(struct v2g_context* v2g_ctx) { + if (check_interface(v2g_ctx) == -1) { + return -1; + } + + if (v2g_ctx->tls_security != TLS_SECURITY_FORCE) { + v2g_ctx->local_tcp_addr = static_cast(calloc(1, sizeof(*v2g_ctx->local_tcp_addr))); + if (v2g_ctx->local_tcp_addr == nullptr) { + dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TCP address"); + return -1; + } + } + + if (v2g_ctx->tls_security != TLS_SECURITY_PROHIBIT) { + v2g_ctx->local_tls_addr = static_cast(calloc(1, sizeof(*v2g_ctx->local_tls_addr))); + if (!v2g_ctx->local_tls_addr) { + dlog(DLOG_LEVEL_ERROR, "Failed to allocate memory for TLS address"); + return -1; + } + } + + while (1) { + if (v2g_ctx->local_tcp_addr) { + get_interface_ipv6_address(v2g_ctx->if_name, ADDR6_TYPE_LINKLOCAL, v2g_ctx->local_tcp_addr); + if (v2g_ctx->local_tls_addr) { + // Handle allowing TCP with TLS (TLS_SECURITY_ALLOW) + memcpy(v2g_ctx->local_tls_addr, v2g_ctx->local_tcp_addr, sizeof(*v2g_ctx->local_tls_addr)); + } + } else { + // Handle forcing TLS security (TLS_SECURITY_FORCE) + get_interface_ipv6_address(v2g_ctx->if_name, ADDR6_TYPE_LINKLOCAL, v2g_ctx->local_tls_addr); + } + + if (v2g_ctx->local_tcp_addr) { + char buffer[INET6_ADDRSTRLEN]; + + /* + * When we bind with port = 0, the kernel assigns a dynamic port from the range configured + * in /proc/sys/net/ipv4/ip_local_port_range. This is on a recent Ubuntu Linux e.g. + * $ cat /proc/sys/net/ipv4/ip_local_port_range + * 32768 60999 + * However, in ISO15118 spec the IANA range with 49152 to 65535 is referenced. So we have the + * problem that the kernel (without further configuration - and we want to avoid this) could + * hand out a port which is not "range compatible". + * To fulfill the ISO15118 standard, we simply try to bind to static port numbers. + */ + v2g_ctx->local_tcp_addr->sin6_port = htons(DEFAULT_TCP_PORT); + v2g_ctx->tcp_socket = connection_create_socket(v2g_ctx->local_tcp_addr); + if (v2g_ctx->tcp_socket < 0) { + /* retry until interface is ready */ + sleep(1); + continue; + } + if (inet_ntop(AF_INET6, &v2g_ctx->local_tcp_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { + dlog(DLOG_LEVEL_INFO, "TCP server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, + v2g_ctx->if_name, buffer, v2g_ctx->local_tcp_addr->sin6_scope_id, + ntohs(v2g_ctx->local_tcp_addr->sin6_port)); + } else { + dlog(DLOG_LEVEL_ERROR, "TCP server on %s is listening, but inet_ntop failed: %s", v2g_ctx->if_name, + strerror(errno)); + return -1; + } + } + + if (v2g_ctx->local_tls_addr) { + char buffer[INET6_ADDRSTRLEN]; + + /* see comment above for reason */ + v2g_ctx->local_tls_addr->sin6_port = htons(DEFAULT_TLS_PORT); + + v2g_ctx->tls_socket.fd = connection_create_socket(v2g_ctx->local_tls_addr); + if (v2g_ctx->tls_socket.fd < 0) { + if (v2g_ctx->tcp_socket != -1) { + /* free the TCP socket */ + close(v2g_ctx->tcp_socket); + } + /* retry until interface is ready */ + sleep(1); + continue; + } + + if (inet_ntop(AF_INET6, &v2g_ctx->local_tls_addr->sin6_addr, buffer, sizeof(buffer)) != nullptr) { + dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening on port [%s%%%" PRIu32 "]:%" PRIu16, + v2g_ctx->if_name, buffer, v2g_ctx->local_tls_addr->sin6_scope_id, + ntohs(v2g_ctx->local_tls_addr->sin6_port)); + } else { + dlog(DLOG_LEVEL_INFO, "TLS server on %s is listening, but inet_ntop failed: %s", v2g_ctx->if_name, + strerror(errno)); + return -1; + } + } + /* Sockets should be ready, leave the loop */ + break; + } + + if (v2g_ctx->local_tls_addr) { + return tls::connection_init(v2g_ctx); + } + return 0; +} + +/*! + * \brief is_sequence_timeout This function checks if a sequence timeout has occured. + * \param ts_start Is the time after waiting of the next request message. + * \param ctx is the V2G context. + * \return Returns \c true if a timeout has occured, otherwise \c false + */ +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx) { + struct timespec ts_current; + int sequence_timeout = V2G_SEQUENCE_TIMEOUT_60S; + + if (((clock_gettime(CLOCK_MONOTONIC, &ts_current)) != 0) || + (timespec_to_ms(timespec_sub(ts_current, ts_start)) > sequence_timeout)) { + dlog(DLOG_LEVEL_ERROR, "Sequence timeout has occured (message: %s)", v2g_msg_type[ctx->current_v2g_msg]); + return true; + } + return false; +} + +/*! + * \brief connection_read This function reads from socket until requested bytes are received or sequence + * timeout is reached + * \param conn is the v2g connection context + * \param buf is the buffer to store the v2g message + * \param count is the number of bytes to read + * \return Returns \c true if a timeout has occured, otherwise \c false + */ +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, size_t count, bool read_complete) { + struct timespec ts_start; + int bytes_read = 0; + + if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) { + dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno)); + return -1; + } + + /* loop until we got all requested bytes or sequence timeout DIN [V2G-DC-432]*/ + if (read_complete) { + while ((bytes_read < count) && (is_sequence_timeout(ts_start, conn->ctx) == false) && + (conn->ctx->is_connection_terminated == false)) { // [V2G2-536] + + int num_of_bytes; + + /* use select for timeout handling */ + struct timeval tv; + fd_set read_fds; + + FD_ZERO(&read_fds); + FD_SET(conn->conn.socket_fd, &read_fds); + + tv.tv_sec = conn->ctx->network_read_timeout / 1000; + tv.tv_usec = (conn->ctx->network_read_timeout % 1000) * 1000; + + num_of_bytes = select(conn->conn.socket_fd + 1, &read_fds, nullptr, nullptr, &tv); + + if (num_of_bytes == -1) { + if (errno == EINTR) + continue; + + return -1; + } + + /* Zero fds ready means we timed out, so let upper loop check our sequence timeout */ + if (num_of_bytes == 0) { + continue; + } + num_of_bytes = (int)read(conn->conn.socket_fd, &buf[bytes_read], count - bytes_read); + + if (num_of_bytes == -1) { + if (errno == EINTR) + continue; + + return -1; + } + + /* return when peer closed connection */ + if (num_of_bytes == 0) + return bytes_read; + + bytes_read += num_of_bytes; + } + } else { + bytes_read = (int)read(conn->conn.socket_fd, buf, count); + } + + if (conn->ctx->is_connection_terminated == true) { + dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted"); + return -2; + } + + return (ssize_t)bytes_read; // [V2G2-537] read bytes are currupted if reading from socket was interrupted + // (V2G_SECC_Sequence_Timeout) +} + +/*! + * \brief connection_read This function writes to socket until bytes are written to the socket + * \param conn is the v2g connection context + * \param buf is the buffer where the v2g message is stored + * \param count is the number of bytes to write + * \return Returns \c true if a timeout has occured, otherwise \c false + */ +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, size_t count) { + int bytes_written = 0; + + /* loop until we got all requested bytes out */ + while (bytes_written < count) { + int num_of_bytes; + + num_of_bytes = (int)write(conn->conn.socket_fd, &buf[bytes_written], count - bytes_written); + + if (num_of_bytes == -1) { + if (errno == EINTR) + continue; + + return -1; + } + + /* return when peer closed connection */ + if (num_of_bytes == 0) + return bytes_written; + + bytes_written += num_of_bytes; + } + + return (ssize_t)bytes_written; +} + +/** + * This is the 'main' function of a thread, which handles a TCP connection. + */ +void* connection_handle_tcp(void* data) { + struct v2g_connection* conn = static_cast(data); + connection_handle(data); + /* tear down connection gracefully */ + dlog(DLOG_LEVEL_INFO, "Multiplexer: Closing TCP connection"); + + std::this_thread::sleep_for(std::chrono::seconds(2)); + + if (shutdown(conn->conn.socket_fd, SHUT_RDWR) == -1) { + dlog(DLOG_LEVEL_ERROR, "shutdown() failed: %s", strerror(errno)); + } + + // Waiting for client closing the connection + std::this_thread::sleep_for(std::chrono::seconds(3)); + + if (close(conn->conn.socket_fd) == -1) { + dlog(DLOG_LEVEL_ERROR, "close() failed: %s", strerror(errno)); + } + dlog(DLOG_LEVEL_INFO, "Multiplexer: TCP connection closed gracefully"); + + free(conn); + return nullptr; +} + +/** + * This is the 'main' function of a thread, which handles a TCP connection. + */ +void* connection_handle(void* data) { + struct v2g_connection* conn = static_cast(data); + int rv = 0; + + bool iso20{false}; + + conn->buffer = static_cast(malloc(DEFAULT_BUFFER_SIZE)); + if (not conn->buffer) { + return nullptr; + } + + /* check if the v2g-session is already running in another thread, if not, handle v2g-connection */ + if (conn->ctx->state == 0) { + iso20 = v2g_detect_iso20_support(conn); + } else { + rv = ERROR_SESSION_ALREADY_STARTED; + dlog(DLOG_LEVEL_WARNING, "%s", "Closing tcp-connection. v2g-session is already running"); + } + + uint16_t port = conn->ctx->proxy_port_iso2; + conn->ctx->selected_iso20 = false; + // Open TCP connection to the proxied module + if (iso20) { + // Notify the proxy layer about the protocol decision + conn->ctx->selected_iso20 = true; + port = conn->ctx->proxy_port_iso20; + } + + int proxy_fd = proxy_connect(port); + + if (proxy_fd > 0) { + EVLOG_info << "Connected to proxy module for " << (conn->ctx->selected_iso20 ? "ISO-20" : "ISO-2/DIN"); + conn->proxy(conn, proxy_fd); + } + + return nullptr; +} + +int connection_proxy(struct v2g_connection* conn, int proxy_fd) { + + dlog(DLOG_LEVEL_INFO, "Multiplexer: Proxy TCP->TCP"); + + int ev_fd = conn->conn.socket_fd; + + // SupportedAppProtocolReq message is still in buffer, we need to forward it to the external stack + write(proxy_fd, conn->buffer, conn->payload_len + 8); + + struct pollfd poll_list[2]; + poll_list[0].fd = proxy_fd; + poll_list[1].fd = ev_fd; + poll_list[0].events = POLLIN; + poll_list[1].events = POLLIN; + + unsigned char buf[2048]; + + while (true) { + + int ret = poll(poll_list, 2, -1); + + if (ret == -1) { + return -1; // poll error + } + + // Timed out, but we blocked forever. This could be a spurious wakeup, so just try again. + if (ret == 0) { + continue; + } + + if (poll_list[0].revents & POLLIN) { + // we can read from proxy (connection to local ISO module) + int nrbytes = read(proxy_fd, buf, sizeof(buf)); + + if (nrbytes == 0) { + break; + } + // write data to EV + nrbytes = conn->write(conn, buf, nrbytes); + } + + if (poll_list[0].revents & POLLERR or poll_list[0].revents & POLLHUP or poll_list[0].revents & POLLNVAL) { + // something is wrong with the TCP connection to the ISO module + return -1; + } + + if (poll_list[1].revents & POLLIN) { + // we can read from EV + int nrbytes = conn->read(conn, buf, sizeof(buf), false); + if (nrbytes == 0) { + break; + } + // write data to proxy + nrbytes = write(proxy_fd, buf, nrbytes); + } + + if (poll_list[1].revents & POLLERR or poll_list[1].revents & POLLHUP or poll_list[1].revents & POLLNVAL) { + // something is wrong with the TCP connection to the EV + return -1; + } + } + + close(proxy_fd); + return 0; +} + +static void* connection_server(void* data) { + struct v2g_context* ctx = static_cast(data); + struct v2g_connection* conn = NULL; + pthread_attr_t attr; + + /* create the thread in detached state so we don't need to join every single one */ + if (pthread_attr_init(&attr) != 0) { + dlog(DLOG_LEVEL_ERROR, "pthread_attr_init failed: %s", strerror(errno)); + goto thread_exit; + } + if (pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED) != 0) { + dlog(DLOG_LEVEL_ERROR, "pthread_attr_setdetachstate failed: %s", strerror(errno)); + goto thread_exit; + } + + while (1) { + char client_addr[INET6_ADDRSTRLEN]; + struct sockaddr_in6 addr; + socklen_t addrlen = sizeof(addr); + + /* cleanup old one and create new connection context */ + free(conn); + conn = static_cast(calloc(1, sizeof(*conn))); + if (!conn) { + dlog(DLOG_LEVEL_ERROR, "Calloc failed: %s", strerror(errno)); + break; + } + + /* setup common stuff */ + conn->ctx = ctx; + conn->read = &connection_read; + conn->write = &connection_write; + conn->proxy = &connection_proxy; + + /* if this thread is the TLS thread, then connections are TLS secured; + * return code is non-zero if equal so align it + */ + conn->is_tls_connection = false; + + /* wait for an incoming connection */ + conn->conn.socket_fd = accept(ctx->tcp_socket, (struct sockaddr*)&addr, &addrlen); + if (conn->conn.socket_fd == -1) { + dlog(DLOG_LEVEL_ERROR, "Accept(tcp) failed: %s", strerror(errno)); + continue; + } + + if (inet_ntop(AF_INET6, &addr, client_addr, sizeof(client_addr)) != NULL) { + dlog(DLOG_LEVEL_INFO, "Incoming connection on %s from [%s]:%" PRIu16, ctx->if_name, client_addr, + ntohs(addr.sin6_port)); + } else { + dlog(DLOG_LEVEL_ERROR, "Incoming connection on %s, but inet_ntop failed: %s", ctx->if_name, + strerror(errno)); + } + + // store the port to create a udp socket + conn->ctx->udp_port = ntohs(addr.sin6_port); + + if (pthread_create(&conn->thread_id, &attr, connection_handle_tcp, conn) != 0) { + dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno)); + continue; + } + + /* is up to the thread to cleanup conn */ + conn = NULL; + } + +thread_exit: + if (pthread_attr_destroy(&attr) != 0) { + dlog(DLOG_LEVEL_ERROR, "pthread_attr_destroy failed: %s", strerror(errno)); + } + + /* clean up if dangling */ + free(conn); + + return NULL; +} + +int connection_start_servers(struct v2g_context* ctx) { + int rv, tcp_started = 0; + + if (ctx->tcp_socket != -1) { + rv = pthread_create(&ctx->tcp_thread, NULL, connection_server, ctx); + if (rv != 0) { + dlog(DLOG_LEVEL_ERROR, "pthread_create(tcp) failed: %s", strerror(errno)); + return -1; + } + tcp_started = 1; + } + + if (ctx->tls_socket.fd != -1) { + rv = tls::connection_start_server(ctx); + if (rv != 0) { + if (tcp_started) { + pthread_cancel(ctx->tcp_thread); + pthread_join(ctx->tcp_thread, NULL); + } + dlog(DLOG_LEVEL_ERROR, "pthread_create(tls) failed: %s", strerror(errno)); + return -1; + } + } + + return 0; +} + +int create_udp_socket(const uint16_t udp_port, const char* interface_name) { + constexpr auto LINK_LOCAL_MULTICAST = "ff02::1"; + + int udp_socket = socket(AF_INET6, SOCK_DGRAM, 0); + if (udp_socket < 0) { + EVLOG_error << "Could not create socket: " << strerror(errno); + return udp_socket; + } + + // source setup + + // find port between 49152-65535 + auto could_bind = false; + auto source_port = 49152; + for (; source_port < 65535; source_port++) { + sockaddr_in6 source_address = {AF_INET6, htons(source_port)}; + if (bind(udp_socket, reinterpret_cast(&source_address), sizeof(sockaddr_in6)) == 0) { + could_bind = true; + break; + } + } + + if (!could_bind) { + EVLOG_error << "Could not bind: " << strerror(errno); + return -1; + } + + EVLOG_info << "UDP socket bound to source port: " << source_port; + + const auto index = if_nametoindex(interface_name); + auto mreq = ipv6_mreq{}; + mreq.ipv6mr_interface = index; + if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &mreq.ipv6mr_multiaddr) <= 0) { + EVLOG_error << "Failed to setup multicast address" << strerror(errno); + return -1; + } + if (setsockopt(udp_socket, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) { + EVLOG_error << "Could not add multicast group membership: " << strerror(errno); + return -1; + } + + if (setsockopt(udp_socket, IPPROTO_IPV6, IPV6_MULTICAST_IF, &index, sizeof(index)) < 0) { + EVLOG_error << "Could not set interface name: " << interface_name << "with error: " << strerror(errno); + } + + // destination setup + sockaddr_in6 destination_address = {AF_INET6, htons(udp_port)}; + if (inet_pton(AF_INET6, LINK_LOCAL_MULTICAST, &destination_address.sin6_addr) <= 0) { + EVLOG_error << "Failed to setup server address" << strerror(errno); + } + const auto connected = + connect(udp_socket, reinterpret_cast(&destination_address), sizeof(sockaddr_in6)) == 0; + if (!connected) { + EVLOG_error << "Could not connect: " << strerror(errno); + return -1; + } + + return udp_socket; +} diff --git a/modules/IsoMux/connection/connection.hpp b/modules/IsoMux/connection/connection.hpp new file mode 100644 index 000000000..1280f8581 --- /dev/null +++ b/modules/IsoMux/connection/connection.hpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022 chargebyte GmbH +// Copyright (C) 2022 Contributors to EVerest + +#ifndef CONNECTION_H +#define CONNECTION_H + +#include +#include + +#include "v2g_ctx.hpp" + +/*! + * \brief initialise TCP/TLS connections + * \param ctx the V2G context + * \return 0 on success + */ +int connection_init(struct v2g_context* ctx); + +/*! + * \brief start TCP/TLS servers + * \param ctx the V2G context + * \return 0 on success + */ +int connection_start_servers(struct v2g_context* ctx); +int create_udp_socket(const uint16_t udp_port, const char* interface_name); + +/*! + * \brief check for V2G message sequence timeout + * \param ts_start start time + * \param ctx the V2G context + * \return true on timeout + */ +bool is_sequence_timeout(struct timespec ts_start, struct v2g_context* ctx); + +/*! + * \brief connection_read This abstracts a read from the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count number of read bytes. + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count, bool read_complete); + +/*! + * \brief connection_write This abstracts a write to the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count size of the buffer + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +void* connection_handle_tcp(void* data); +void* connection_handle(void* data); +int connection_proxy(struct v2g_connection* conn, int proxy_fd); + +#endif /* CONNECTION_H */ diff --git a/modules/IsoMux/connection/proxy.hpp b/modules/IsoMux/connection/proxy.hpp new file mode 100644 index 000000000..a74bec3c3 --- /dev/null +++ b/modules/IsoMux/connection/proxy.hpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022 chargebyte GmbH +// Copyright (C) 2022 Contributors to EVerest + +#ifndef ISOMUX_PROXY_H +#define ISOMUX_PROXY_H + +#include +#include + +/*! + * \brief connect to a local V2G server + * \param port port to connect to + * \return 0 on failure, otherwise the socket + */ +inline int proxy_connect(uint16_t port) { + + int sock_fd = -1; + struct sockaddr_in6 server_addr; + + /* Create socket for communication with server */ + sock_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (sock_fd == -1) { + perror("socket()"); + return -1; + } + + /* Connect to server running on localhost */ + server_addr.sin6_family = AF_INET6; + inet_pton(AF_INET6, "::1", &server_addr.sin6_addr); + server_addr.sin6_port = htons(port); + + /* Try to do TCP handshake with server */ + int ret = connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)); + if (ret == -1) { + perror("connect()"); + close(sock_fd); + return -1; + } + + return sock_fd; +} + +#endif /* ISOMUX_PROXY_H */ diff --git a/modules/IsoMux/connection/tls_connection.cpp b/modules/IsoMux/connection/tls_connection.cpp new file mode 100644 index 000000000..d98abfc9f --- /dev/null +++ b/modules/IsoMux/connection/tls_connection.cpp @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "tls_connection.hpp" +#include "connection.hpp" +#include "log.hpp" +#include "v2g.hpp" +#include "v2g_server.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +// used when ctx->network_read_timeout_tls is 0 +constexpr int default_timeout_ms = 1000; + +void process_connection_thread(std::shared_ptr con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + + openssl::pkey_ptr contract_public_key{nullptr, nullptr}; + auto connection = std::make_unique(); + connection->ctx = ctx; + connection->is_tls_connection = true; + connection->read = &tls::connection_read; + connection->write = &tls::connection_write; + connection->proxy = &tls::connection_proxy; + connection->tls_connection = con.get(); + connection->pubkey = &contract_public_key; + + dlog(DLOG_LEVEL_INFO, "Incoming TLS connection"); + + bool loop{true}; + while (loop) { + loop = false; + const auto result = con->accept(); + switch (result) { + case tls::Connection::result_t::success: + + // TODO(james-ctc) v2g_ctx->tls_key_logging + + if (ctx->state == 0) { + const auto rv = ::connection_handle(connection.get()); + dlog(DLOG_LEVEL_INFO, "connection_handle exited with %d", rv); + } else { + dlog(DLOG_LEVEL_INFO, "%s", "Closing tls-connection. v2g-session is already running"); + } + + con->shutdown(); + break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + loop = con->wait_for(result, default_timeout_ms) == tls::Connection::result_t::success; + break; + case tls::Connection::result_t::closed: + case tls::Connection::result_t::timeout: + default: + break; + } + } +} + +void handle_new_connection_cb(tls::Server::ConnectionPtr&& con, struct v2g_context* ctx) { + assert(con != nullptr); + assert(ctx != nullptr); + // create a thread to process this connection + try { + // passing unique pointers through thread parameters is problematic + std::shared_ptr connection(con.release()); + std::thread connection_loop(process_connection_thread, connection, ctx); + connection_loop.detach(); + } catch (const std::system_error&) { + // unable to start thread + dlog(DLOG_LEVEL_ERROR, "pthread_create() failed: %s", strerror(errno)); + con->shutdown(); + } +} + +void server_loop_thread(struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + const auto res = ctx->tls_server->serve([ctx](auto con) { handle_new_connection_cb(std::move(con), ctx); }); + if (res != tls::Server::state_t::stopped) { + dlog(DLOG_LEVEL_ERROR, "tls::Server failed to serve"); + } +} + +bool build_config(tls::Server::config_t& config, struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->r_security != nullptr); + + using types::evse_security::CaCertificateType; + using types::evse_security::EncodingFormat; + using types::evse_security::GetCertificateInfoStatus; + using types::evse_security::LeafCertificateType; + + /* + * libevse-security checks for an optional password and when one + * isn't set is uses an empty string as the password rather than nullptr. + * hence private keys are always encrypted. + */ + + bool bResult{false}; + + config.cipher_list = "ECDHE-ECDSA-AES128-SHA256:ECDH-ECDSA-AES128-SHA256"; + config.ciphersuites = ""; // disable TLS 1.3 + config.verify_client = false; // contract certificate managed in-band in 15118-2 + + // use the existing configured socket + // TODO(james-ctc): switch to server socket init code otherwise there + // may be issues with reinitialisation + config.socket = ctx->tls_socket.fd; + config.io_timeout_ms = static_cast(ctx->network_read_timeout_tls); + + config.tls_key_logging = ctx->tls_key_logging; + + // information from libevse-security + const auto cert_info = + ctx->r_security->call_get_leaf_certificate_info(LeafCertificateType::V2G, EncodingFormat::PEM, false); + if (cert_info.status != GetCertificateInfoStatus::Accepted) { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Not Accepted"); + } else { + if (cert_info.info) { + const auto& info = cert_info.info.value(); + const auto cert_path = info.certificate.value_or(""); + const auto key_path = info.key; + + // workaround (see above libevse-security comment) + const auto key_password = info.password.value_or(""); + + auto& ref = config.chains.emplace_back(); + ref.certificate_chain_file = cert_path.c_str(); + ref.private_key_file = key_path.c_str(); + ref.private_key_password = key_password.c_str(); + + if (info.ocsp) { + for (const auto& ocsp : info.ocsp.value()) { + const char* file{nullptr}; + if (ocsp.ocsp_path) { + file = ocsp.ocsp_path.value().c_str(); + } + ref.ocsp_response_files.push_back(file); + } + } + + bResult = true; + } else { + dlog(DLOG_LEVEL_ERROR, "Failed to read cert_info! Empty response"); + } + } + + return bResult; +} + +tls::Server::OptionalConfig configure_ssl(struct v2g_context* ctx) { + try { + dlog(DLOG_LEVEL_WARNING, "configure_ssl"); + auto config = std::make_unique(); + + // The config of interest is from Evse Security, no point in updating + // config when there is a problem + + if (build_config(*config, ctx)) { + return {{std::move(config)}}; + } + } catch (const std::bad_alloc&) { + dlog(DLOG_LEVEL_ERROR, "unable to create TLS config"); + } + return std::nullopt; +} + +} // namespace + +namespace tls { + +int connection_init(struct v2g_context* ctx) { + using state_t = tls::Server::state_t; + + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + assert(ctx->r_security != nullptr); + + int res{-1}; + tls::Server::config_t config; + + // build_config can fail due to issues with Evse Security, + // this can be retried later. Not treated as an error. + (void)build_config(config, ctx); + + // apply config + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + const auto result = ctx->tls_server->init(config, [ctx]() { return configure_ssl(ctx); }); + if ((result == state_t::init_complete) || (result == state_t::init_socket)) { + res = 0; + } + + return res; +} + +int connection_start_server(struct v2g_context* ctx) { + assert(ctx != nullptr); + assert(ctx->tls_server != nullptr); + + // only starts the TLS server + + int res = 0; + try { + ctx->tls_server->stop(); + ctx->tls_server->wait_stopped(); + if (ctx->tls_server->state() == tls::Server::state_t::stopped) { + // need to re-initialise + tls::connection_init(ctx); + } + std::thread serve_loop(server_loop_thread, ctx); + serve_loop.detach(); + ctx->tls_server->wait_running(); + } catch (const std::system_error&) { + // unable to start thread (caller logs failure) + res = -1; + } + return res; +} + +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, const std::size_t count, bool read_complete) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_read{0}; + timespec ts_start{}; + + if (clock_gettime(CLOCK_MONOTONIC, &ts_start) == -1) { + dlog(DLOG_LEVEL_ERROR, "clock_gettime(ts_start) failed: %s", strerror(errno)); + result = -1; + } + + while ((bytes_read < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_read; + std::size_t bytes_in{0}; + auto* ptr = reinterpret_cast(&buf[bytes_read]); + + const auto read_res = conn->tls_connection->read(ptr, remaining, bytes_in); + switch (read_res) { + case tls::Connection::result_t::success: + bytes_read += bytes_in; + break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + conn->tls_connection->wait_for(read_res, default_timeout_ms); + break; + case tls::Connection::result_t::timeout: + // the MBedTLS code loops on timeout, is_sequence_timeout() is used instead + break; + case tls::Connection::result_t::closed: + default: + result = -1; + break; + } + + if (conn->ctx->is_connection_terminated) { + dlog(DLOG_LEVEL_ERROR, "Reading from tcp-socket aborted"); + conn->tls_connection->shutdown(); + result = -2; + } + + if (::is_sequence_timeout(ts_start, conn->ctx)) { + break; + } + + if (not read_complete) { + break; + } + } + + return (result < 0) ? result : static_cast(bytes_read); +} + +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count) { + assert(conn != nullptr); + assert(conn->tls_connection != nullptr); + + ssize_t result{0}; + std::size_t bytes_written{0}; + + while ((bytes_written < count) && (result >= 0)) { + const std::size_t remaining = count - bytes_written; + std::size_t bytes_out{0}; + const auto* ptr = reinterpret_cast(&buf[bytes_written]); + + const auto write_res = conn->tls_connection->write(ptr, remaining, bytes_out); + switch (write_res) { + case tls::Connection::result_t::success: + bytes_written += bytes_out; + break; + case tls::Connection::result_t::want_read: + case tls::Connection::result_t::want_write: + conn->tls_connection->wait_for(write_res, default_timeout_ms); + break; + case tls::Connection::result_t::timeout: + // the MBedTLS code loops on timeout + break; + case tls::Connection::result_t::closed: + default: + result = -1; + break; + } + } + + if ((result == -1) && (conn->tls_connection->state() == tls::Connection::state_t::closed)) { + // if the connection has closed - return the number of bytes sent + result = 0; + } + + return (result < 0) ? result : static_cast(bytes_written); +} + +int connection_proxy(struct v2g_connection* conn, int proxy_fd) { + + dlog(DLOG_LEVEL_INFO, "Multiplexer: Proxy TLS->TCP"); + int ev_fd = conn->tls_connection->socket(); // underlying socket of TLS connection + + // SupportedAppProtocolReq message is still in buffer, we need to forward it to the external stack + write(proxy_fd, conn->buffer, conn->payload_len + 8); + + struct pollfd poll_list[2]; + poll_list[0].fd = proxy_fd; + poll_list[1].fd = ev_fd; + poll_list[0].events = POLLIN; + poll_list[1].events = POLLIN; + + unsigned char buf[2048]; + + // Set reading to (more or less) non-blocking + conn->tls_connection->set_read_timeout(10); + + while (true) { + // Note we cannot simply poll on the underlying system socket for TLS connection + // as it does not guarantee that SSL_read/write will not block after the poll + // (an SSL_read my trigger an actual write or multiple reads on the system socket) + // So we have to try a non-blocking SSL_read first, openssl will then tell us + // what to wait for on the socket before we try again (read, write or both) + + auto r = conn->read(conn, buf, sizeof(buf), false); + + if (r < 0) { + // something is wrong with the connection, exiting... + break; + } else if (r > 0) { + // successfully read bytes, forward to proxy module + write(proxy_fd, buf, r); + } + + // check if SSL was actually waiting on write + int e = SSL_get_error(conn->tls_connection->ssl_context(), r); + if (e == SSL_ERROR_WANT_WRITE) { + poll_list[1].events = POLLIN | POLLOUT; + } else { + poll_list[1].events = POLLIN; + } + + int ret = poll(poll_list, 2, -1); + + if (ret == -1) { + return -1; // poll error + } + + // Timed out, but we blocked forever. This could be a spurious wakeup, so just try again. + if (ret == 0) { + continue; + } + + if (poll_list[0].revents & POLLIN) { + // we can read from proxy (connection to local ISO module) + int nrbytes = read(proxy_fd, buf, sizeof(buf)); + + if (nrbytes == 0) { + break; + } + // write data to EV + nrbytes = conn->write(conn, buf, nrbytes); + } + + if (poll_list[0].revents & POLLERR or poll_list[0].revents & POLLHUP or poll_list[0].revents & POLLNVAL) { + // something is wrong with the TCP connection to the ISO module + return -1; + } + + if (poll_list[1].revents & POLLIN or poll_list[1].revents & POLLOUT) { + // we can read from / write to the EV raw socket, just continue here. + // The actual SSL_read() will happen at the beginning of the loop + continue; + } + + if (poll_list[1].revents & POLLERR or poll_list[1].revents & POLLHUP or poll_list[1].revents & POLLNVAL) { + // something is wrong with the TCP connection to the EV + return -1; + } + } + + close(proxy_fd); + return 0; +} + +} // namespace tls diff --git a/modules/IsoMux/connection/tls_connection.hpp b/modules/IsoMux/connection/tls_connection.hpp new file mode 100644 index 000000000..d14bf6794 --- /dev/null +++ b/modules/IsoMux/connection/tls_connection.hpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef TLS_CONNECTION_HPP_ +#define TLS_CONNECTION_HPP_ + +#include +#include + +struct v2g_context; +struct v2g_connection; + +namespace tls { + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_init(struct v2g_context* ctx); + +/*! + * \param ctx v2g connection context + * \return returns 0 on succss and -1 on error + */ +int connection_start_server(struct v2g_context* ctx); + +/*! + * \brief connection_read This abstracts a read from the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count number of read bytes. + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_read(struct v2g_connection* conn, unsigned char* buf, std::size_t count, bool read_complete); + +/*! + * \brief connection_write This abstracts a write to the connection socket, so that higher level functions + * are not required to distinguish between TCP and TLS connections. + * \param conn v2g connection context + * \param buf buffer to store received message sequence. + * \param count size of the buffer + * \return Returns the number of read bytes if successful, otherwise returns -1 for reading errors and + * -2 for closed connection */ +ssize_t connection_write(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + +int connection_proxy(struct v2g_connection* conn, int proxy_fd); + +} // namespace tls + +#endif // TLS_CONNECTION_HPP_ diff --git a/modules/IsoMux/log.cpp b/modules/IsoMux/log.cpp new file mode 100644 index 000000000..9a6b3d396 --- /dev/null +++ b/modules/IsoMux/log.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#include "log.hpp" +#include // for logging +#include // for va_list, va_{start,end}() +#include // for v*printf() +#include // for atoi() +#include // for strlen() +#include // for gettimeofday() +#include // for strftime() + +dloglevel_t minloglevel_current = DLOG_LEVEL_INFO; + +static const char* debug_level_logstring_map[DLOG_LEVEL_NUMLEVELS] = { + // tailing space, no need to add it later when printing + // try to keep the strings almost same length, looks better + "[(LOG)] ", "[ERROR] ", "[WARN] ", "[INFO] ", "[DEBUG] ", "[TRACE] "}; + +const char* debug_level_mqtt_string_map[DLOG_LEVEL_NUMLEVELS] = {"always", "error", "warning", + "info", "debug", "trace"}; + +// FIXME: inline? +void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname, + const char* format, ...) { + // fast exit + if (loglevel > minloglevel_current) { + return; + } + char* format_copy = NULL; + FILE* outstream = stderr; // change output target here, if desired + + struct timeval debug_tval; + struct tm tm; + char log_datetimestamp[16]; // length due to format [00:00:00.000], rounded up to fit 32-bit alignment + gettimeofday(&debug_tval, NULL); // ignore return value + size_t offset = + strftime(log_datetimestamp, sizeof(log_datetimestamp), "[%H:%M:%S", gmtime_r(&debug_tval.tv_sec, &tm)); + if (offset < 1) { + // in our use of strftime(), this is an error + return; + } + + snprintf(log_datetimestamp + offset, sizeof(log_datetimestamp) - offset, ".%03ld] ", debug_tval.tv_usec / 1000); + + va_list args; + va_start(args, format); + + // print the user given part + // strip possible newline character from user-given string + // FIXME: could be skipped + if (format) { + size_t formatlen = std::string(format).size(); + format_copy = static_cast(calloc(1, formatlen + 1)); // additional byte for terminating \0 + memcpy(format_copy, format, formatlen); + if ((formatlen >= 1) && (format_copy[formatlen - 1] == '\n')) { + format_copy[formatlen - 1] = '\0'; + } + } + char output[256]; + if (format_copy != NULL) { + vsnprintf(output, sizeof(output), format_copy, args); + } + // force EOL + fputs("\n", outstream); + fflush(outstream); + va_end(args); + if (format_copy) { + free(format_copy); + } + + switch (loglevel) { + case DLOG_LEVEL_ERROR: + EVLOG_error << output; + break; + case DLOG_LEVEL_WARNING: + EVLOG_warning << output; + break; + case DLOG_LEVEL_INFO: + EVLOG_info << output; + break; + case DLOG_LEVEL_DEBUG: + EVLOG_debug << output; + break; + case DLOG_LEVEL_TRACE: + EVLOG_verbose << output; + break; + default: + EVLOG_critical << "Unknown log level"; + break; + } +} + +void dlog_level_inc(void) { + dloglevel_t minloglevel_new = (dloglevel_t)((int)minloglevel_current + 1); + if (minloglevel_new == DLOG_LEVEL_NUMLEVELS) { + // wrap to bottom, but not DLOG_LEVEL_ALWAYS + minloglevel_new = DLOG_LEVEL_ERROR; + } + dlog_level_set(minloglevel_new); +} + +void dlog_level_set(const dloglevel_t loglevel) { + // no sanity checks currently + const dloglevel_t minloglevel_old = minloglevel_current; + dloglevel_t newloglevel = loglevel; + if (newloglevel >= DLOG_LEVEL_NUMLEVELS) { + // set something illegally high + newloglevel = (dloglevel_t)(int)(DLOG_LEVEL_NUMLEVELS - 1); + } + if (newloglevel <= DLOG_LEVEL_ALWAYS) { + // set something illegally low + newloglevel = (dloglevel_t)(int)(DLOG_LEVEL_ALWAYS + 1); + } + if (newloglevel != minloglevel_current) { + minloglevel_current = newloglevel; + dlog(DLOG_LEVEL_ALWAYS, "switched log level from %d (\"%s\") to %d (\"%s\")", minloglevel_old, + debug_level_logstring_map[minloglevel_old], newloglevel, debug_level_logstring_map[newloglevel]); + } +} + +dloglevel_t dlog_level_get(void) { + return minloglevel_current; +} + +static const char* dlog_level_get_string(const dloglevel_t loglevel) { + if ((loglevel < 1) || loglevel >= DLOG_LEVEL_NUMLEVELS) { + return "invalid_level"; + } + return debug_level_mqtt_string_map[loglevel]; +} diff --git a/modules/IsoMux/log.hpp b/modules/IsoMux/log.hpp new file mode 100644 index 000000000..d4963c3d4 --- /dev/null +++ b/modules/IsoMux/log.hpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#ifndef LOG_H +#define LOG_H + +/** + * @brief Describe the intended log level of a message, or the maximum level a message must have to be displayed. + */ +typedef enum dloglevel_t { + DLOG_LEVEL_ALWAYS = 0, ///< internal use only, for notification of log level change + DLOG_LEVEL_ERROR, ///< error + DLOG_LEVEL_WARNING, ///< warning, not leading to unexpected behavior such as termination + DLOG_LEVEL_INFO, ///< informational message + DLOG_LEVEL_DEBUG, ///< message to help debug daemon activity + DLOG_LEVEL_TRACE, ///< message to provide extra internal information + DLOG_LEVEL_NUMLEVELS, ///< don't use, only for internal detection of upper range +} dloglevel_t; + +/** + * @brief Internal: Issue a log message. Please use the dlog() macro instead. + * + * @return void + */ +void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname, + const char* format, ...); + +/** + * @brief Increase the log level to the next higher step (more messages). At the highest step, the level rolls over to + * the lowest. + * + * @return void + */ +void dlog_level_inc(void); + +/** + * @brief Set the log level. + * @param[in] loglevel the log level the logger shall use, of type enum dloglevel + * + * @return void + */ +void dlog_level_set(const dloglevel_t loglevel); + +/** + * @brief Get the log level. + * + * @return dloglevel_t the currently valid log level + */ +dloglevel_t dlog_level_get(void); + +/** + * @brief Set the log level from an MQTT topic string. + * @param[in] loglevel the log level the logger shall use, as an MQTT string + * + * @return void + */ +// dloglevel_t dlog_level_set_from_mqtt_string(const char *level_string); + +/** + * @brief Issue a log message. + * + * @param[in] level the log level this message belongs to (type enum dloglevel) + * @param[in] printf()-like format string and parameters, without tailing '\n' + * + * @return void + */ +// this is a macro, so that when dlog() is used, it gets expanded at the caller's location +#define dlog(level, ...) \ + do { \ + dlog_func((level), __FILE__, __LINE__, __func__, ##__VA_ARGS__); \ + } while (0) + +#endif /* LOG_H */ diff --git a/modules/IsoMux/manifest.yaml b/modules/IsoMux/manifest.yaml new file mode 100644 index 000000000..3904f5c69 --- /dev/null +++ b/modules/IsoMux/manifest.yaml @@ -0,0 +1,58 @@ +description: >- + This module is a multiplexer to support switching over between different ISO module implementations +config: + device: + description: >- + Ethernet device used for HLC. Any local interface that has an ipv6 + link-local and a MAC addr will work + type: string + default: eth0 + tls_security: + description: >- + Controls how to handle encrypted communication + type: string + enum: + - prohibit + - allow + - force + default: prohibit + tls_key_logging: + description: >- + Enable/Disable the export of TLS session keys (pre-master-secret) + during a TLS handshake. This log file can be used to decrypt TLS + sessions. Note that this option is for testing and simulation + purpose only + type: boolean + default: false + tls_timeout: + description: >- + Set the TLS timeout in ms when establishing a tls connection + type: integer + default: 15000 + proxy_port_iso2: + description: >- + TCP port of the local ISO2 instance + type: integer + default: 61341 + proxy_port_iso20: + description: >- + TCP port of the local ISO20 instance + type: integer + default: 50000 +provides: + charger: + interface: ISO15118_charger + description: >- + This module implements the ISO15118-2 implementation of + an AC or DC charger +requires: + security: + interface: evse_security + iso2: + interface: ISO15118_charger + iso20: + interface: ISO15118_charger +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - Cornelius Claussen diff --git a/modules/IsoMux/sdp.cpp b/modules/IsoMux/sdp.cpp new file mode 100644 index 000000000..dc48d1e8b --- /dev/null +++ b/modules/IsoMux/sdp.cpp @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#include "sdp.hpp" +#include "log.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define DEBUG 1 + +/* defines for V2G SDP implementation */ +#define SDP_SRV_PORT 15118 + +#define SDP_VERSION 0x01 +#define SDP_INVERSE_VERSION 0xfe + +#define SDP_HEADER_LEN 8 +#define SDP_REQUEST_PAYLOAD_LEN 2 +#define SDP_RESPONSE_PAYLOAD_LEN 20 + +#define SDP_REQUEST_TYPE 0x9000 +#define SDP_RESPONSE_TYPE 0x9001 + +#define POLL_TIMEOUT 20 + +enum sdp_security { + SDP_SECURITY_TLS = 0x00, + SDP_SECURITY_NONE = 0x10, +}; + +enum sdp_transport_protocol { + SDP_TRANSPORT_PROTOCOL_TCP = 0x00, + SDP_TRANSPORT_PROTOCOL_UDP = 0x10, +}; + +/* link-local multicast address ff02::1 aka ip6-allnodes */ +#define IN6ADDR_ALLNODES \ + { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 } + +/* bundles various aspects of a SDP query */ +struct sdp_query { + struct v2g_context* v2g_ctx; + + struct sockaddr_in6 remote_addr; + + enum sdp_security security_requested; + enum sdp_transport_protocol proto_requested; +}; + +/* + * Fills the SDP header into a given buffer + */ +static int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len) { + int offset = 0; + + buffer[offset++] = SDP_VERSION; + buffer[offset++] = SDP_INVERSE_VERSION; + + /* payload is network byte order */ + buffer[offset++] = (payload_type >> 8) & 0xff; + buffer[offset++] = payload_type & 0xff; + + /* payload_length is network byte order */ + buffer[offset++] = (payload_len >> 24) & 0xff; + buffer[offset++] = (payload_len >> 16) & 0xff; + buffer[offset++] = (payload_len >> 8) & 0xff; + buffer[offset++] = payload_len & 0xff; + + return offset; +} + +static int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len) { + uint16_t payload_type; + uint32_t payload_len; + + if (buffer[0] != SDP_VERSION) { + dlog(DLOG_LEVEL_ERROR, "Invalid SDP version"); + return -1; + } + + if (buffer[1] != SDP_INVERSE_VERSION) { + dlog(DLOG_LEVEL_ERROR, "Invalid SDP inverse version"); + return -1; + } + + payload_type = (buffer[2] << 8) + buffer[3]; + if (payload_type != expected_payload_type) { + dlog(DLOG_LEVEL_ERROR, "Invalid payload type: expected %" PRIu16 ", received %" PRIu16, expected_payload_type, + payload_type); + return -1; + } + + payload_len = (buffer[4] << 24) + (buffer[5] << 16) + (buffer[6] << 8) + buffer[7]; + if (payload_len != expected_payload_len) { + dlog(DLOG_LEVEL_ERROR, "Invalid payload length: expected %" PRIu32 ", received %" PRIu32, expected_payload_len, + payload_len); + return -1; + } + + return 0; +} + +int sdp_create_response(uint8_t* buffer, struct sockaddr_in6* addr, enum sdp_security security, + enum sdp_transport_protocol proto) { + int offset = SDP_HEADER_LEN; + + /* fill in first the payload */ + + /* address is already network byte order */ + memcpy(&buffer[offset], &addr->sin6_addr, sizeof(addr->sin6_addr)); + offset += sizeof(addr->sin6_addr); + + memcpy(&buffer[offset], &addr->sin6_port, sizeof(addr->sin6_port)); + offset += sizeof(addr->sin6_port); + + buffer[offset++] = security; + buffer[offset++] = proto; + + /* now fill in the header with payload length */ + sdp_write_header(buffer, SDP_RESPONSE_TYPE, offset - SDP_HEADER_LEN); + + return offset; +} + +/* + * Sends a SDP response packet + */ +int sdp_send_response(int sdp_socket, struct sdp_query* sdp_query) { + uint8_t buffer[SDP_HEADER_LEN + SDP_RESPONSE_PAYLOAD_LEN]; + int rv = 0; + + /* at the moment we only understand TCP protocol */ + if (sdp_query->proto_requested != SDP_TRANSPORT_PROTOCOL_TCP) { + dlog(DLOG_LEVEL_ERROR, "SDP requested unsupported protocol 0x%02x, announcing nothing", + sdp_query->proto_requested); + return 1; + } + + switch (sdp_query->security_requested) { + case SDP_SECURITY_TLS: + if (sdp_query->v2g_ctx->local_tls_addr) { + dlog(DLOG_LEVEL_INFO, "SDP requested TLS, announcing TLS"); + sdp_create_response(buffer, sdp_query->v2g_ctx->local_tls_addr, SDP_SECURITY_TLS, + SDP_TRANSPORT_PROTOCOL_TCP); + break; + } + if (sdp_query->v2g_ctx->local_tcp_addr) { + dlog(DLOG_LEVEL_INFO, "SDP requested TLS, announcing NO-TLS"); + sdp_create_response(buffer, sdp_query->v2g_ctx->local_tcp_addr, SDP_SECURITY_NONE, + SDP_TRANSPORT_PROTOCOL_TCP); + break; + } + dlog(DLOG_LEVEL_ERROR, "SDP requested TLS, announcing nothing"); + return 1; + + case SDP_SECURITY_NONE: + if (sdp_query->v2g_ctx->local_tcp_addr) { + dlog(DLOG_LEVEL_INFO, "SDP requested NO-TLS, announcing NO-TLS"); + sdp_create_response(buffer, sdp_query->v2g_ctx->local_tcp_addr, SDP_SECURITY_NONE, + SDP_TRANSPORT_PROTOCOL_TCP); + break; + } + if (sdp_query->v2g_ctx->local_tls_addr) { + dlog(DLOG_LEVEL_INFO, "SDP requested NO-TLS, announcing TLS"); + sdp_create_response(buffer, sdp_query->v2g_ctx->local_tls_addr, SDP_SECURITY_TLS, + SDP_TRANSPORT_PROTOCOL_TCP); + break; + } + dlog(DLOG_LEVEL_ERROR, "SDP requested NO-TLS, announcing nothing"); + return 1; + + default: + dlog(DLOG_LEVEL_ERROR, "SDP requested unsupported security 0x%02x, announcing nothing", + sdp_query->security_requested); + return 1; + } + + if (sendto(sdp_socket, buffer, sizeof(buffer), 0, (struct sockaddr*)&sdp_query->remote_addr, + sizeof(struct sockaddr_in6)) != sizeof(buffer)) { + rv = -1; + } + if (DEBUG) { + char addrbuf[INET6_ADDRSTRLEN] = {0}; + const char* addr; + int saved_errno = errno; + + addr = inet_ntop(AF_INET6, &sdp_query->remote_addr.sin6_addr, addrbuf, sizeof(addrbuf)); + if (rv == 0) { + dlog(DLOG_LEVEL_INFO, "sendto([%s]:%" PRIu16 ") succeeded", addr, ntohs(sdp_query->remote_addr.sin6_port)); + } else { + dlog(DLOG_LEVEL_ERROR, "sendto([%s]:%" PRIu16 ") failed: %s", addr, ntohs(sdp_query->remote_addr.sin6_port), + strerror(saved_errno)); + } + } + + return rv; +} + +int sdp_init(struct v2g_context* v2g_ctx) { + struct sockaddr_in6 sdp_addr = {AF_INET6, htons(SDP_SRV_PORT)}; + struct ipv6_mreq mreq = {{IN6ADDR_ALLNODES}, 0}; + int enable = 1; + + mreq.ipv6mr_interface = if_nametoindex(v2g_ctx->if_name); + if (!mreq.ipv6mr_interface) { + dlog(DLOG_LEVEL_ERROR, "No such interface: %s", v2g_ctx->if_name); + return -1; + } + + /* create receiving socket */ + v2g_ctx->sdp_socket = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (v2g_ctx->sdp_socket == -1) { + dlog(DLOG_LEVEL_ERROR, "socket() failed: %s", strerror(errno)); + return -1; + } + + if (setsockopt(v2g_ctx->sdp_socket, SOL_SOCKET, SO_REUSEPORT, &enable, sizeof(enable)) == -1) { + dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_REUSEPORT) failed: %s", strerror(errno)); + close(v2g_ctx->sdp_socket); + return -1; + } + + sdp_addr.sin6_addr = in6addr_any; + + if (bind(v2g_ctx->sdp_socket, (struct sockaddr*)&sdp_addr, sizeof(sdp_addr)) == -1) { + dlog(DLOG_LEVEL_ERROR, "bind() failed: %s", strerror(errno)); + close(v2g_ctx->sdp_socket); + return -1; + } + + dlog(DLOG_LEVEL_INFO, "SDP socket setup succeeded"); + + /* bind only to specified device */ + if (setsockopt(v2g_ctx->sdp_socket, SOL_SOCKET, SO_BINDTODEVICE, v2g_ctx->if_name, strlen(v2g_ctx->if_name)) == + -1) { + dlog(DLOG_LEVEL_ERROR, "setsockopt(SO_BINDTODEVICE) failed: %s", strerror(errno)); + close(v2g_ctx->sdp_socket); + return -1; + } + + dlog(DLOG_LEVEL_TRACE, "bind only to specified device"); + + /* join multicast group */ + if (setsockopt(v2g_ctx->sdp_socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) == -1) { + dlog(DLOG_LEVEL_ERROR, "setsockopt(IPV6_JOIN_GROUP) failed: %s", strerror(errno)); + close(v2g_ctx->sdp_socket); + return -1; + } + + dlog(DLOG_LEVEL_TRACE, "joined multicast group"); + + return 0; +} + +int sdp_listen(struct v2g_context* v2g_ctx) { + /* Init pollfd struct */ + struct pollfd pollfd = {v2g_ctx->sdp_socket, POLLIN, 0}; + + while (!v2g_ctx->shutdown) { + uint8_t buffer[SDP_HEADER_LEN + SDP_REQUEST_PAYLOAD_LEN]; + char addrbuf[INET6_ADDRSTRLEN] = {0}; + const char* addr = addrbuf; + struct sdp_query sdp_query = { + .v2g_ctx = v2g_ctx, + }; + socklen_t addrlen = sizeof(sdp_query.remote_addr); + + /* Check if data was received on socket */ + signed status = poll(&pollfd, 1, POLL_TIMEOUT); + + if (status == -1) { + if (errno == EINTR) { // If the call did not succeed because it was interrupted + continue; + } else { + dlog(DLOG_LEVEL_ERROR, "poll() failed: %s", strerror(errno)); + continue; + } + } + /* If new data was received, handle sdp request */ + if (status > 0) { + ssize_t len = recvfrom(v2g_ctx->sdp_socket, buffer, sizeof(buffer), 0, + (struct sockaddr*)&sdp_query.remote_addr, &addrlen); + if (len == -1) { + if (errno != EINTR) + dlog(DLOG_LEVEL_ERROR, "recvfrom() failed: %s", strerror(errno)); + continue; + } + + addr = inet_ntop(AF_INET6, &sdp_query.remote_addr.sin6_addr, addrbuf, sizeof(addrbuf)); + + if (len != sizeof(buffer)) { + dlog(DLOG_LEVEL_WARNING, "Discarded packet from [%s]:%" PRIu16 " due to unexpected length %zd", addr, + ntohs(sdp_query.remote_addr.sin6_port), len); + continue; + } + + if (sdp_validate_header(buffer, SDP_REQUEST_TYPE, SDP_REQUEST_PAYLOAD_LEN)) { + dlog(DLOG_LEVEL_WARNING, "Packet with invalid SDP header received from [%s]:%" PRIu16, addr, + ntohs(sdp_query.remote_addr.sin6_port)); + continue; + } + + sdp_query.security_requested = (sdp_security)buffer[SDP_HEADER_LEN + 0]; + sdp_query.proto_requested = (sdp_transport_protocol)buffer[SDP_HEADER_LEN + 1]; + + dlog(DLOG_LEVEL_INFO, "Received packet from [%s]:%" PRIu16 " with security 0x%02x and protocol 0x%02x", + addr, ntohs(sdp_query.remote_addr.sin6_port), sdp_query.security_requested, sdp_query.proto_requested); + + sdp_send_response(v2g_ctx->sdp_socket, &sdp_query); + } + } + + if (close(v2g_ctx->sdp_socket) == -1) { + dlog(DLOG_LEVEL_ERROR, "close() failed: %s", strerror(errno)); + } + + return 0; +} diff --git a/modules/IsoMux/sdp.hpp b/modules/IsoMux/sdp.hpp new file mode 100644 index 000000000..a689d3097 --- /dev/null +++ b/modules/IsoMux/sdp.hpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#ifndef SDP_H +#define SDP_H + +#include "v2g.hpp" + +int sdp_init(struct v2g_context* v2g_ctx); +int sdp_listen(struct v2g_context* v2g_ctx); + +#endif /* SDP_H */ diff --git a/modules/IsoMux/tools.cpp b/modules/IsoMux/tools.cpp new file mode 100644 index 000000000..f94db476d --- /dev/null +++ b/modules/IsoMux/tools.cpp @@ -0,0 +1,376 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#include "tools.hpp" +#include "log.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ssize_t safe_read(int fd, void* buf, size_t count) { + for (;;) { + ssize_t result = read(fd, buf, count); + + if (result >= 0) + return result; + else if (errno == EINTR) + continue; + else + return result; + } +} + +int generate_random_data(void* dest, size_t dest_len) { + size_t len = 0; + int fd; + + fd = open("/dev/urandom", O_RDONLY); + if (fd == -1) + return -1; + + while (len < dest_len) { + ssize_t rv = safe_read(fd, dest, dest_len); + + if (rv < 0) { + close(fd); + return -1; + } + + len += rv; + } + + close(fd); + return 0; +} + +unsigned int generate_srand_seed(void) { + unsigned int s; + + if (generate_random_data(&s, sizeof(s)) == -1) + return 42; /* just to _not_ use 1 which is the default value when srand is not used at all */ + + return s; +} + +const char* choose_first_ipv6_interface() { + struct ifaddrs *ifaddr, *ifa; + char buffer[INET6_ADDRSTRLEN]; + + if (getifaddrs(&ifaddr) == -1) + return NULL; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + + if (ifa->ifa_addr->sa_family == AF_INET6) { + inet_ntop(AF_INET6, &ifa->ifa_addr->sa_data, buffer, sizeof(buffer)); + if (strstr(buffer, "fe80") != NULL) { + return ifa->ifa_name; + } + } + } + dlog(DLOG_LEVEL_ERROR, "No necessary IPv6 link-local address was found!"); + return NULL; +} + +int get_interface_ipv6_address(const char* if_name, enum Addr6Type type, struct sockaddr_in6* addr) { + struct ifaddrs *ifaddr, *ifa; + int rv = -1; + + if (getifaddrs(&ifaddr) == -1) + return -1; + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (!ifa->ifa_addr) + continue; + + if (ifa->ifa_addr->sa_family != AF_INET6) + continue; + + if (strcmp(ifa->ifa_name, if_name) != 0) + continue; + + /* on Linux the scope_id is interface index for link-local addresses */ + switch (type) { + case ADDR6_TYPE_GLOBAL: /* no link-local address requested */ + if ((reinterpret_cast(ifa->ifa_addr))->sin6_scope_id != 0) + continue; + break; + + case ADDR6_TYPE_LINKLOCAL: /* link-local address requested */ + if ((reinterpret_cast(ifa->ifa_addr))->sin6_scope_id == 0) + continue; + break; + + default: /* any address of the interface requested */ + /* use first found */ + break; + } + + memcpy(addr, ifa->ifa_addr, sizeof(*addr)); + + rv = 0; + goto out; + } + +out: + freeifaddrs(ifaddr); + return rv; +} + +#define NSEC_PER_SEC 1000000000L + +void set_normalized_timespec(struct timespec* ts, time_t sec, int64_t nsec) { + while (nsec >= NSEC_PER_SEC) { + nsec -= NSEC_PER_SEC; + ++sec; + } + while (nsec < 0) { + nsec += NSEC_PER_SEC; + --sec; + } + ts->tv_sec = sec; + ts->tv_nsec = nsec; +} + +struct timespec timespec_add(struct timespec lhs, struct timespec rhs) { + struct timespec ts_delta; + + set_normalized_timespec(&ts_delta, lhs.tv_sec + rhs.tv_sec, lhs.tv_nsec + rhs.tv_nsec); + + return ts_delta; +} + +struct timespec timespec_sub(struct timespec lhs, struct timespec rhs) { + struct timespec ts_delta; + + set_normalized_timespec(&ts_delta, lhs.tv_sec - rhs.tv_sec, lhs.tv_nsec - rhs.tv_nsec); + + return ts_delta; +} + +void timespec_add_ms(struct timespec* ts, long long msec) { + long long sec = msec / 1000; + + set_normalized_timespec(ts, ts->tv_sec + sec, ts->tv_nsec + (msec - sec * 1000) * 1000 * 1000); +} + +/* + * lhs < rhs: return < 0 + * lhs == rhs: return 0 + * lhs > rhs: return > 0 + */ +int timespec_compare(const struct timespec* lhs, const struct timespec* rhs) { + if (lhs->tv_sec < rhs->tv_sec) + return -1; + if (lhs->tv_sec > rhs->tv_sec) + return 1; + return lhs->tv_nsec - rhs->tv_nsec; +} + +long long timespec_to_ms(struct timespec ts) { + return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000); +} + +long long timespec_to_us(struct timespec ts) { + return ((long long)ts.tv_sec * 1000000) + (ts.tv_nsec / 1000); +} + +int msleep(int ms) { + struct timespec req, rem; + + req.tv_sec = ms / 1000; + req.tv_nsec = (ms % 1000) * (1000 * 1000); /* x ms */ + + while ((nanosleep(&req, &rem) == (-1)) && (errno == EINTR)) { + req = rem; + } + + return 0; +} + +long long int getmonotonictime() { + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_sec * 1000 + time.tv_nsec / 1000000; +} + +double calc_physical_value(const int16_t& value, const int8_t& multiplier) { + return static_cast(value * pow(10.0, multiplier)); +} + +bool range_check_int32(int32_t min, int32_t max, int32_t value) { + return ((value < min) || (value > max)) ? false : true; +} + +bool range_check_int64(int64_t min, int64_t max, int64_t value) { + return ((value < min) || (value > max)) ? false : true; +} + +void round_down(const char* buffer, size_t len) { + char* p; + + p = (char*)strchr(buffer, '.'); + + if (!p) + return; + + if (p - buffer > len - 2) + return; + + if (*(p + 1) == '\0') + return; + + *(p + 2) = '\0'; +} + +bool get_dir_filename(char* file_name, uint8_t file_name_len, const char* path, const char* file_name_identifier) { + + file_name[0] = '\0'; + + if (path == NULL) { + dlog(DLOG_LEVEL_ERROR, "Invalid file path"); + return false; + } + DIR* d = opendir(path); // open the path + + if (d == NULL) { + dlog(DLOG_LEVEL_ERROR, "Unable to open file path %s", path); + return false; + } + struct dirent* dir; // for the directory entries + uint8_t file_name_identifier_len = std::string(file_name_identifier).size(); + while ((dir = readdir(d)) != NULL) { + if (dir->d_type != DT_DIR) { + /* if the type is not directory*/ + if ((std::string(dir->d_name).size() > (file_name_identifier_len)) && /* Plus one for the numbering */ + (strncmp(file_name_identifier, dir->d_name, file_name_identifier_len) == 0) && + (file_name_len > std::string(dir->d_name).size())) { + strncpy(file_name, dir->d_name, std::string(dir->d_name).size() + 1); + break; + } + } + } + + closedir(d); + + return (file_name[0] != '\0'); +} + +uint8_t get_dir_numbered_file_names(char file_names[MAX_PKI_CA_LENGTH][MAX_FILE_NAME_LENGTH], const char* path, + const char* prefix, const char* suffix, const uint8_t offset, + const uint8_t max_idx) { + if (path == NULL) { + dlog(DLOG_LEVEL_ERROR, "Invalid file path"); + return 0; + } + + DIR* d = opendir(path); // open the path + + if (d == NULL) { + dlog(DLOG_LEVEL_ERROR, "Unable to open file path %s", path); + return 0; + } + struct dirent* dir; // for the directory entries + uint8_t num_of_files = 0; + uint8_t prefix_len = std::string(prefix).size(); + uint8_t suffix_len = std::string(suffix).size(); + uint8_t min_idx = max_idx; // helper value to re-sort array + + while (((dir = readdir(d)) != NULL) && (num_of_files != (max_idx - offset))) { + if (dir->d_type != DT_DIR) { + /* if the type is not directory*/ + if ((std::string(dir->d_name).size() > (prefix_len + suffix_len + 1)) && /* Plus one for the numbering */ + (0 == strncmp(prefix, dir->d_name, prefix_len))) { + for (uint8_t idx = offset; idx < max_idx; idx++) { + /* Iterated over the number prefix */ + if ((dir->d_name[prefix_len] == ('0' + idx)) && + (strncmp(suffix, &dir->d_name[prefix_len + 1], suffix_len) == 0)) { + if (MAX_FILE_NAME_LENGTH >= std::string(dir->d_name).size()) { + strcpy(file_names[idx], dir->d_name); + // dlog(DLOG_LEVEL_ERROR,"Cert-file found: %s", &AFileNames[idx]); + num_of_files++; + min_idx = std::min(min_idx, idx); + } else { + dlog(DLOG_LEVEL_ERROR, "Max. file-name size exceeded. Only %i characters supported", + MAX_FILE_NAME_LENGTH); + num_of_files = 0; + goto exit; + } + } + } + } + } else if (dir->d_type == DT_DIR && strcmp(dir->d_name, ".") != 0 && strcmp(dir->d_name, "..") != 0) { + /*if it is a directory*/ + } + } + /* Re-sort array. This part fills gaps in the array. For example if there is only one file with + * number _4_, the following code will cpy the file name of index 3 of the AFileNames array to + * index 0 (In case AOffset is set to 0) */ + if (min_idx != offset) { + for (uint8_t idx = offset; (idx - offset) < num_of_files; idx++) { + strcpy(file_names[idx], file_names[min_idx + idx - offset]); + } + } + +exit: + closedir(d); + + return num_of_files + offset; +} + +std::string convert_to_hex_str(const uint8_t* data, int len) { + std::stringstream string_stream; + string_stream << std::hex; + + for (int idx = 0; idx < len; ++idx) + string_stream << std::setw(2) << std::setfill('0') << (int)data[idx]; + + return string_stream.str(); +} + +types::iso15118_charger::HashAlgorithm +convert_to_hash_algorithm(const types::evse_security::HashAlgorithm hash_algorithm) { + switch (hash_algorithm) { + case types::evse_security::HashAlgorithm::SHA256: + return types::iso15118_charger::HashAlgorithm::SHA256; + case types::evse_security::HashAlgorithm::SHA384: + return types::iso15118_charger::HashAlgorithm::SHA384; + case types::evse_security::HashAlgorithm::SHA512: + return types::iso15118_charger::HashAlgorithm::SHA512; + default: + throw std::runtime_error( + "Could not convert types::evse_security::HashAlgorithm to types::iso15118_charger::HashAlgorithm"); + } +} + +std::vector +convert_to_certificate_hash_data_info_vector(const types::evse_security::OCSPRequestDataList& ocsp_request_data_list) { + std::vector certificate_hash_data_info_vec; + for (const auto& ocsp_request_data : ocsp_request_data_list.ocsp_request_data_list) { + if (ocsp_request_data.responder_url.has_value() and ocsp_request_data.certificate_hash_data.has_value()) { + types::iso15118_charger::CertificateHashDataInfo certificate_hash_data; + certificate_hash_data.hashAlgorithm = + convert_to_hash_algorithm(ocsp_request_data.certificate_hash_data.value().hash_algorithm); + certificate_hash_data.issuerNameHash = ocsp_request_data.certificate_hash_data.value().issuer_name_hash; + certificate_hash_data.issuerKeyHash = ocsp_request_data.certificate_hash_data.value().issuer_key_hash; + certificate_hash_data.serialNumber = ocsp_request_data.certificate_hash_data.value().serial_number; + certificate_hash_data.responderURL = ocsp_request_data.responder_url.value(); + certificate_hash_data_info_vec.push_back(certificate_hash_data); + } + } + return certificate_hash_data_info_vec; +} diff --git a/modules/IsoMux/tools.hpp b/modules/IsoMux/tools.hpp new file mode 100644 index 000000000..b9365e233 --- /dev/null +++ b/modules/IsoMux/tools.hpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#ifndef TOOLS_H +#define TOOLS_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_FILE_NAME_LENGTH 100 +#define MAX_PKI_CA_LENGTH 4 /* leaf up to root certificate */ + +#ifndef ARRAY_SIZE +#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0])) +#endif + +#ifndef ROUND_UP +#define ROUND_UP(N, S) ((((N) + (S)-1) / (S)) * (S)) +#endif + +#ifndef ROUND_UP_ELEMENTS +#define ROUND_UP_ELEMENTS(N, S) (((N) + (S)-1) / (S)) +#endif + +int generate_random_data(void* dest, size_t dest_len); +unsigned int generate_srand_seed(void); + +enum Addr6Type { + ADDR6_TYPE_UNPSEC = -1, + ADDR6_TYPE_GLOBAL = 0, + ADDR6_TYPE_LINKLOCAL = 1, +}; + +const char* choose_first_ipv6_interface(); +int get_interface_ipv6_address(const char* if_name, enum Addr6Type type, struct sockaddr_in6* addr); + +void set_normalized_timespec(struct timespec* ts, time_t sec, int64_t nsec); +int timespec_compare(const struct timespec* lhs, const struct timespec* rhs); +struct timespec timespec_sub(struct timespec lhs, struct timespec rhs); +struct timespec timespec_add(struct timespec lhs, struct timespec rhs); +void timespec_add_ms(struct timespec* ts, long long msec); +long long timespec_to_ms(struct timespec ts); +long long timespec_to_us(struct timespec ts); +int msleep(int ms); +long long int getmonotonictime(void); + +/*! + * \brief calc_physical_value This function calculates the physical value consists on a value and multiplier. + * \param value is the value of the physical value + * \param multiplier is the multiplier of the physical value + * \return Returns the physical value + */ +double calc_physical_value(const int16_t& value, const int8_t& multiplier); + +/*! + * \brief range_check_int32 This function checks if an int32 value is within the given range. + * \param min is the min value. + * \param max is the max value. + * \param value which must be checked. + * \return Returns \c true if it is within range, otherwise \c false. + */ +bool range_check_int32(int32_t min, int32_t max, int32_t value); + +/*! + * \brief range_check_int64 This function checks if an int64 value is within the given range. + * \param min is the min value. + * \param max is the max value. + * \param value which must be checked. + * \return Returns \c true if it is within range, otherwise \c false. + */ +bool range_check_int64(int64_t min, int64_t max, int64_t value); + +/*! + * \brief round_down "round" a string representation of a float down to 1 decimal places + * \param buffer is the float string + * \param len is the length of the buffer + */ +void round_down(const char* buffer, size_t len); + +/*! + * \brief get_dir_filename This function searches for a specific name (AFileNameIdentifier) in a file path and stores + * the complete name with file ending in \c AFileName + * \param file_name is the buffer to write the file name. + * \param file_name_len is the length of the buffer. + * \param path is the file path which will be used to search for the specific file. + * \param file_name_identifier is the identifier of the file (file without file ending). + * \return Returns \c true if the file could be found, otherwise \c false. + */ +bool get_dir_filename(char* file_name, uint8_t file_name_len, const char* path, const char* file_name_identifier); + +/*! + * \brief get_dir_numbered_file_names This helper-function searches for numbered files in the given file path and stores + * the file names in given char array + * \param file_names is the char array for the findings. + * \param path is the path where the numbered files are stored. + * \param prefix is the prefix of the numbered file. + * \param suffix is the suffix of the numbered file. + * \param offset defines the starting number of the file name. + * \param max_idx is the max index of a file (Between 0-9). + * \return Returns the number of files which where found in the file path. + */ +uint8_t get_dir_numbered_file_names(char file_names[MAX_PKI_CA_LENGTH][MAX_FILE_NAME_LENGTH], const char* path, + const char* prefix, const char* suffix, const uint8_t offset, + const uint8_t max_idx); + +/*! + * \brief convert_to_hex_str This function converts a array of binary data to hex string. + * \param data is the array of binary data. + * \param len is length of the array. + * \return Returns the converted string. + */ +std::string convert_to_hex_str(const uint8_t* data, int len); + +/** + * \brief convert the given \p hash_algorithm to type types::iso15118_charger::HashAlgorithm + * \param hash_algorithm + * \return types::iso15118_charger::HashAlgorithm + */ +types::iso15118_charger::HashAlgorithm +convert_to_hash_algorithm(const types::evse_security::HashAlgorithm hash_algorithm); + +/** + * \brief convert the given \p ocsp_request_data_list to std::vector + * \param ocsp_request_data_list + * \return std::vector + */ +std::vector +convert_to_certificate_hash_data_info_vector(const types::evse_security::OCSPRequestDataList& ocsp_request_data_list); + +#endif /* TOOLS_H */ diff --git a/modules/IsoMux/v2g.hpp b/modules/IsoMux/v2g.hpp new file mode 100644 index 000000000..58be0cdbc --- /dev/null +++ b/modules/IsoMux/v2g.hpp @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#ifndef V2G_H +#define V2G_H + +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +/* timeouts in milliseconds */ +#define V2G_SEQUENCE_TIMEOUT_60S 60000 /* [V2G2-443] et.al. */ +#define V2G_SEQUENCE_TIMEOUT_10S 10000 + +#define ISO_15118_2013_MSG_DEF "urn:iso:15118:2:2013:MsgDef" +#define ISO_15118_2013_MAJOR 2 + +#define ISO_15118_2010_MSG_DEF "urn:iso:15118:2:2010:MsgDef" +#define ISO_15118_2010_MAJOR 1 + +#define DIN_70121_MSG_DEF "urn:din:70121:2012:MsgDef" +#define DIN_70121_MAJOR 2 + +#define EVSE_LEAF_KEY_FILE_NAME "CPO_EVSE_LEAF.key" +#define EVSE_PROV_KEY_FILE_NAME "PROV_LEAF.key" +#define MO_ROOT_CRT_NAME "MO_ROOT_CRT" +#define V2G_ROOT_CRT_NAME "V2G_ROOT_CRT" +#define MAX_V2G_ROOT_CERTS 10 +#define MAX_KEY_PW_LEN 32 +#define FORCE_PUB_MSG 25 // max msg cycles when topics values must be udpated +#define MAX_PCID_LEN 17 + +#define DEFAULT_BUFFER_SIZE 8192 + +#define DEBUG 1 + +enum tls_security_level { + TLS_SECURITY_ALLOW = 0, + TLS_SECURITY_PROHIBIT, + TLS_SECURITY_FORCE +}; + +enum v2g_event { + V2G_EVENT_NO_EVENT = 0, + V2G_EVENT_TERMINATE_CONNECTION, // Terminate the connection immediately + V2G_EVENT_SEND_AND_TERMINATE, // Send next msg and terminate the connection + V2G_EVENT_SEND_RECV_EXI_MSG, // If msg must not be exi-encoded and can be sent directly + V2G_EVENT_IGNORE_MSG // Received message can't be handled +}; + +enum v2g_protocol { + V2G_PROTO_DIN70121 = 0, + V2G_PROTO_ISO15118_2010, + V2G_PROTO_ISO15118_2013, + V2G_PROTO_ISO15118_2015, + V2G_UNKNOWN_PROTOCOL +}; + +/*! + * \brief The res_msg_ids enum is a list of response msg ids + */ +enum V2gMsgTypeId { + V2G_SUPPORTED_APP_PROTOCOL_MSG = 0, + V2G_SESSION_SETUP_MSG, + V2G_SERVICE_DISCOVERY_MSG, + V2G_SERVICE_DETAIL_MSG, + V2G_PAYMENT_SERVICE_SELECTION_MSG, + V2G_PAYMENT_DETAILS_MSG, + V2G_AUTHORIZATION_MSG, + V2G_CHARGE_PARAMETER_DISCOVERY_MSG, + V2G_METERING_RECEIPT_MSG, + V2G_CERTIFICATE_UPDATE_MSG, + V2G_CERTIFICATE_INSTALLATION_MSG, + V2G_CHARGING_STATUS_MSG, + V2G_CABLE_CHECK_MSG, + V2G_PRE_CHARGE_MSG, + V2G_POWER_DELIVERY_MSG, + V2G_CURRENT_DEMAND_MSG, + V2G_WELDING_DETECTION_MSG, + V2G_SESSION_STOP_MSG, + V2G_UNKNOWN_MSG +}; + +/* Struct for tls-session-log-key tracing */ +typedef struct keylogDebugCtx { + FILE* file; + bool inClientRandom; + bool inMasterSecret; + uint8_t hexdumpLinesToProcess; + int udp_socket; + std::string udp_buffer; +} keylogDebugCtx; + +/** + * Abstracts a charging port, i.e. a power outlet in this daemon. + * + * **** NOTE **** + * Be very careful about adding C++ objects since constructors and + * destructors are not called. (see v2g_ctx_create() and calloc) + */ +struct v2g_context { + std::atomic_bool shutdown; + + evse_securityIntf* r_security; + + struct event* com_setup_timeout; + + uint16_t proxy_port_iso2; + uint16_t proxy_port_iso20; + + const char* if_name; + struct sockaddr_in6* local_tcp_addr; + struct sockaddr_in6* local_tls_addr; + + std::string certs_path; + + uint32_t network_read_timeout; /* in milli seconds */ + uint32_t network_read_timeout_tls; /* in milli seconds */ + bool selected_iso20{false}; + + enum tls_security_level tls_security; + + int sdp_socket; + int tcp_socket; + + int udp_port; + int udp_socket; + + pthread_t tcp_thread; + + struct { + int fd; + } tls_socket; + tls::Server* tls_server; + + bool tls_key_logging; + + enum V2gMsgTypeId current_v2g_msg; /* holds the last v2g msg type */ + int state; /* holds the current state id */ + std::atomic_bool is_connection_terminated; /* Is set to true if the connection is terminated (CP State A/F, shutdown + immediately without response message) */ +}; + +/** + * High-level abstraction of an incoming TCP/TLS connection on a certain charging port. + */ +struct v2g_connection { + pthread_t thread_id; + struct v2g_context* ctx; + + bool is_tls_connection; + + // used for non-TLS connections + struct { + int socket_fd; + } conn; + + tls::Connection* tls_connection; + openssl::pkey_ptr* pubkey; + + ssize_t (*read)(struct v2g_connection* conn, unsigned char* buf, std::size_t count, bool read_complete); + ssize_t (*write)(struct v2g_connection* conn, unsigned char* buf, std::size_t count); + int (*proxy)(struct v2g_connection* conn, int proxy_fd); + + /* V2GTP EXI encoding/decoding stuff */ + uint8_t* buffer; + uint32_t payload_len; + exi_bitstream_t stream; + + struct appHand_exiDocument handshake_req; + struct appHand_exiDocument handshake_resp; +}; + +#endif /* V2G_H */ diff --git a/modules/IsoMux/v2g_ctx.cpp b/modules/IsoMux/v2g_ctx.cpp new file mode 100644 index 000000000..02711c6ee --- /dev/null +++ b/modules/IsoMux/v2g_ctx.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest + +#include +#include +#include +#include +#include +#include // sleep + +#include "log.hpp" +#include "v2g_ctx.hpp" + +#include + +struct v2g_context* v2g_ctx_create(evse_securityIntf* r_security) { + struct v2g_context* ctx; + + // TODO There are c++ objects within v2g_context and calloc doesn't call initialisers. + // free() will not call destructors + ctx = static_cast(calloc(1, sizeof(*ctx))); + if (!ctx) + return NULL; + + ctx->r_security = r_security; + ctx->tls_security = TLS_SECURITY_PROHIBIT; // default + + ctx->local_tcp_addr = NULL; + ctx->local_tls_addr = NULL; + + /* interface from config file or options */ + ctx->if_name = "eth1"; + + ctx->network_read_timeout = 1000; + ctx->network_read_timeout_tls = 5000; + + ctx->sdp_socket = -1; + ctx->tcp_socket = -1; + ctx->tls_socket.fd = -1; + + ctx->com_setup_timeout = NULL; + + return ctx; + +free_out: + free(ctx->local_tls_addr); + free(ctx->local_tcp_addr); + free(ctx); + return NULL; +} + +void v2g_ctx_free(struct v2g_context* ctx) { + free(ctx->local_tls_addr); + ctx->local_tls_addr = NULL; + free(ctx->local_tcp_addr); + ctx->local_tcp_addr = NULL; + free(ctx); +} diff --git a/modules/IsoMux/v2g_ctx.hpp b/modules/IsoMux/v2g_ctx.hpp new file mode 100644 index 000000000..1fac4d647 --- /dev/null +++ b/modules/IsoMux/v2g_ctx.hpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2022-2023 chargebyte GmbH +// Copyright (C) 2022-2023 Contributors to EVerest +#ifndef V2G_CTX_H +#define V2G_CTX_H + +#include "v2g.hpp" + +#include + +#define PHY_VALUE_MULT_MIN -3 +#define PHY_VALUE_MULT_MAX 3 +#define PHY_VALUE_VALUE_MIN SHRT_MIN +#define PHY_VALUE_VALUE_MAX SHRT_MAX + +struct v2g_context* v2g_ctx_create(evse_securityIntf* r_security); + +/*! + * \brief v2g_ctx_free + * \param ctx + */ +void v2g_ctx_free(struct v2g_context* ctx); + +#endif /* V2G_CTX_H */ diff --git a/modules/IsoMux/v2g_server.cpp b/modules/IsoMux/v2g_server.cpp new file mode 100644 index 000000000..ec5ad5016 --- /dev/null +++ b/modules/IsoMux/v2g_server.cpp @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest +#include "v2g_server.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "connection.hpp" +#include "log.hpp" +#include "tools.hpp" + +#define MAX_RES_TIME 98 + +/*! + * \brief v2g_incoming_v2gtp This function reads the V2G transport header + * \param conn hold the context of the V2G-connection. + * \return Returns 0 if the V2G-session was successfully stopped, otherwise -1. + */ +static int v2g_incoming_v2gtp(struct v2g_connection* conn) { + assert(conn != nullptr); + assert(conn->read != nullptr); + + int rv; + + /* read and process header */ + rv = conn->read(conn, conn->buffer, V2GTP_HEADER_LENGTH, true); + if (rv < 0) { + dlog(DLOG_LEVEL_ERROR, "connection_read(header) failed: %s", + (rv == -1) ? strerror(errno) : "connection terminated"); + return -1; + } + /* peer closed connection */ + if (rv == 0) + return 1; + if (rv != V2GTP_HEADER_LENGTH) { + dlog(DLOG_LEVEL_ERROR, "connection_read(header) too short: expected %d, got %d", V2GTP_HEADER_LENGTH, rv); + return -1; + } + + rv = V2GTP_ReadHeader(conn->buffer, &conn->payload_len); + if (rv == -1) { + dlog(DLOG_LEVEL_ERROR, "Invalid v2gtp header"); + return -1; + } + + if (conn->payload_len >= UINT32_MAX - V2GTP_HEADER_LENGTH) { + dlog(DLOG_LEVEL_ERROR, "Prevent integer overflow - payload too long: have %d, would need %u", + DEFAULT_BUFFER_SIZE, conn->payload_len); + return -1; + } + + if (conn->payload_len + V2GTP_HEADER_LENGTH > DEFAULT_BUFFER_SIZE) { + dlog(DLOG_LEVEL_ERROR, "payload too long: have %d, would need %u", DEFAULT_BUFFER_SIZE, + conn->payload_len + V2GTP_HEADER_LENGTH); + + /* we have no way to flush/discard remaining unread data from the socket without reading it in chunks, + * but this opens the chance to bind us in a "endless" read loop; so to protect us, simply close the connection + */ + + return -1; + } + /* read request */ + rv = conn->read(conn, &conn->buffer[V2GTP_HEADER_LENGTH], conn->payload_len, true); + if (rv < 0) { + dlog(DLOG_LEVEL_ERROR, "connection_read(payload) failed: %s", + (rv == -1) ? strerror(errno) : "connection terminated"); + return -1; + } + if (rv != conn->payload_len) { + dlog(DLOG_LEVEL_ERROR, "connection_read(payload) too short: expected %d, got %d", conn->payload_len, rv); + return -1; + } + /* adjust buffer pos to decode request */ + conn->stream.byte_pos = V2GTP_HEADER_LENGTH; + conn->stream.data_size = conn->payload_len + V2GTP_HEADER_LENGTH; + + return 0; +} + +/*! + * \brief v2g_handle_apphandshake After receiving a supportedAppProtocolReq message, + * the SECC shall process the received information. DIN [V2G-DC-436] ISO [V2G2-540] + * \param conn hold the context of the v2g-connection. + * \return Returns a v2g-event of type enum v2g_event. + */ +static bool v2g_sniff_apphandshake(struct v2g_connection* conn, bool& iso20) { + int i; + iso20 = false; + + /* validate handshake request and create response */ + init_appHand_exiDocument(&conn->handshake_resp); + conn->handshake_resp.supportedAppProtocolRes_isUsed = 1; + conn->handshake_resp.supportedAppProtocolRes.ResponseCode = + appHand_responseCodeType_Failed_NoNegotiation; // [V2G2-172] + + dlog(DLOG_LEVEL_INFO, "Handling SupportedAppProtocolReq"); + conn->ctx->current_v2g_msg = V2G_SUPPORTED_APP_PROTOCOL_MSG; + + if (decode_appHand_exiDocument(&conn->stream, &conn->handshake_req) != 0) { + dlog(DLOG_LEVEL_ERROR, "decode_appHandExiDocument() failed"); + return false; // If the mesage can't be decoded we have to terminate the tcp-connection + // (e.g. after an unexpected message) + } + + for (i = 0; i < conn->handshake_req.supportedAppProtocolReq.AppProtocol.arrayLen; i++) { + struct appHand_AppProtocolType* app_proto = &conn->handshake_req.supportedAppProtocolReq.AppProtocol.array[i]; + char* proto_ns = strndup(static_cast(app_proto->ProtocolNamespace.characters), + app_proto->ProtocolNamespace.charactersLen); + + if (!proto_ns) { + dlog(DLOG_LEVEL_ERROR, "out-of-memory condition"); + return V2G_EVENT_TERMINATE_CONNECTION; + } + + dlog(DLOG_LEVEL_INFO, + "handshake_req: Namespace: %s, Version: %" PRIu32 ".%" PRIu32 ", SchemaID: %" PRIu8 ", Priority: %" PRIu8, + proto_ns, app_proto->VersionNumberMajor, app_proto->VersionNumberMinor, app_proto->SchemaID, + app_proto->Priority); + + // Check if it supports ISO-20 + const char* iso20_urn = "urn:iso:std:iso:15118:-20"; + if (strncmp(iso20_urn, proto_ns, strlen(iso20_urn)) == 0) { + iso20 = true; + return true; + } + + free(proto_ns); + } + return true; +} + +bool v2g_detect_iso20_support(struct v2g_connection* conn) { + int rv = -1; + enum v2g_event rvAppHandshake = V2G_EVENT_NO_EVENT; + enum v2g_protocol selected_protocol = V2G_UNKNOWN_PROTOCOL; + + /* static setup */ + conn->stream.data = conn->buffer; + bool app_protocol_received = false; + do { + /* setup for receive */ + conn->stream.data[0] = 0; + conn->payload_len = 0; + exi_bitstream_init(&conn->stream, conn->buffer, 0, 0, nullptr); + + /* next call return -1 on error, 1 when peer closed connection, 0 on success */ + rv = v2g_incoming_v2gtp(conn); + + if (rv != 0) { + dlog(DLOG_LEVEL_ERROR, "v2g_incoming_v2gtp() failed"); + } + + if (conn->ctx->is_connection_terminated == true) { + rv = -1; + } + + bool iso20 = false; + app_protocol_received = v2g_sniff_apphandshake(conn, iso20); + + if (iso20) { + return true; + } + + } while ((rv == 1) && not app_protocol_received); + return false; +} diff --git a/modules/IsoMux/v2g_server.hpp b/modules/IsoMux/v2g_server.hpp new file mode 100644 index 000000000..903decdc7 --- /dev/null +++ b/modules/IsoMux/v2g_server.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 chargebyte GmbH +// Copyright (C) 2023 Contributors to EVerest +#ifndef V2G_SERVER_H +#define V2G_SERVER_H + +#include "v2g.hpp" + +static const char* v2g_msg_type[] = { + "Supported App Protocol", + "Session Setup", + "Service Discovery", + "Service Detail", + "Payment Service Selection", + "Payment Details", + "Authorization", + "Charge Parameter Discovery", + "Metering Receipt", + "Certificate Update", + "Certificate Installation", + "Charging Status", + "Cable Check", + "Pre Charge", + "Power Delivery", + "Current Demand", + "Welding Detection", + "Session Stop", + "Unknown", +}; + +bool v2g_detect_iso20_support(struct v2g_connection* conn); + +#endif /* V2G_SERVER_H */ diff --git a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp index 6b37de369..7416668d7 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp +++ b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.cpp @@ -6,22 +6,10 @@ namespace module::main { void LemDCBM400600Controller::init() { - try { - this->time_sync_helper->set_time_config_params(config.meter_timezone, config.meter_dst); - call_with_retry([this]() { this->fetch_meter_id_from_device(); }, this->config.init_number_of_http_retries, - this->config.init_retry_wait_in_milliseconds); - } catch (HttpClientError& http_client_error) { - EVLOG_error << "Initialization of LemDCBM400600Controller failed with http " - "client error: " - << http_client_error.what(); - throw; - } catch (DCBMUnexpectedResponseException& dcbm_error) { - EVLOG_error << "Initialization of LemDCBM400600Controller failed due an " - "unexpected device response: " - << dcbm_error.what(); - throw; - } - + EVLOG_info << "LEM DCBM 400/600: Try to communicate with the device"; + this->time_sync_helper->set_time_config_params(config.meter_timezone, config.meter_dst); + call_with_retry([this]() { this->fetch_meter_id_from_device(); }, this->config.init_number_of_http_retries, + this->config.init_retry_wait_in_milliseconds); this->time_sync_helper->restart_unsafe_period(); } @@ -188,6 +176,7 @@ LemDCBM400600Controller::stop_transaction(const std::string& transaction_id) { this->request_device_to_stop_transaction(tid); } auto signed_meter_value = types::units_signed::SignedMeterValue{fetch_ocmf_result(tid), "", "OCMF"}; + signed_meter_value.public_key.emplace(public_key_ocmf); return types::powermeter::TransactionStopResponse{types::powermeter::TransactionRequestStatus::OK, {}, // Empty start_signed_meter_value signed_meter_value}; diff --git a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp index 34f1b18dd..29e31c1a5 100644 --- a/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp +++ b/modules/LemDCBM400600/main/lem_dcbm_400600_controller.hpp @@ -151,6 +151,9 @@ class LemDCBM400600Controller { types::powermeter::TransactionStartResponse start_transaction(const types::powermeter::TransactionReq& value); types::powermeter::TransactionStopResponse stop_transaction(const std::string& transaction_id); types::powermeter::Powermeter get_powermeter(); + inline bool is_initialized() { + return ("" != meter_id); + } inline std::string get_public_key_ocmf() { return public_key_ocmf; } diff --git a/modules/LemDCBM400600/main/powermeterImpl.cpp b/modules/LemDCBM400600/main/powermeterImpl.cpp index ccf421ea1..7a074f227 100644 --- a/modules/LemDCBM400600/main/powermeterImpl.cpp +++ b/modules/LemDCBM400600/main/powermeterImpl.cpp @@ -27,32 +27,37 @@ void powermeterImpl::init() { mod->config.resilience_transaction_request_retries, mod->config.resilience_transaction_request_retry_delay, mod->config.cable_id, mod->config.tariff_id, mod->config.meter_timezone, mod->config.meter_dst, mod->config.SC, mod->config.UV, mod->config.UD}); - - this->controller->init(); } void powermeterImpl::ready() { // Start the live_measure_publisher thread, which periodically publishes the live measurements of the device this->live_measure_publisher_thread = std::thread([this] { - this->publish_public_key_ocmf(this->controller->get_public_key_ocmf()); while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); try { - this->publish_powermeter(this->controller->get_powermeter()); - // if the communication error is set, clear the error - if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault", - "Communication timed out")) { - // need to update LEM status since we have recovered from a communication loss - this->controller->update_lem_status(); - clear_error("powermeter/CommunicationFault", "Communication timed out"); + if (!this->controller->is_initialized()) { + this->controller->init(); + this->publish_public_key_ocmf(this->controller->get_public_key_ocmf()); + std::this_thread::sleep_for( + std::chrono::milliseconds(mod->config.resilience_initial_connection_retry_delay)); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + this->publish_powermeter(this->controller->get_powermeter()); + // if the communication error is set, clear the error + if (this->error_state_monitor->is_error_active("powermeter/CommunicationFault", + "Communication timed out")) { + // need to update LEM status since we have recovered from a communication loss + this->controller->update_lem_status(); + clear_error("powermeter/CommunicationFault", "Communication timed out"); + } } } catch (LemDCBM400600Controller::DCBMUnexpectedResponseException& dcbm_exception) { EVLOG_error << "Failed to publish powermeter value due to an invalid device response: " << dcbm_exception.what(); } catch (HttpClientError& client_error) { - EVLOG_error << "Failed to publish powermeter value due to an http error: " << client_error.what(); if (!this->error_state_monitor->is_error_active("powermeter/CommunicationFault", "Communication timed out")) { + EVLOG_error << "Failed to communicate with the powermeter due to http error: " + << client_error.what(); auto error = this->error_factory->create_error("powermeter/CommunicationFault", "Communication timed out", "This error is raised due to communication timeout"); diff --git a/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.cpp b/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.cpp index 1b543320f..6caa646f1 100644 --- a/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.cpp +++ b/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.cpp @@ -25,6 +25,42 @@ void power_supply_DCImpl::init() { p.voltage_V = v; mod->p_powermeter->publish_powermeter(p); }); + + std::thread([this]() { + float low_pass_voltage = 0.; + + float last_low_pass_voltage = -1; + + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // prevent overshoot + if (low_pass_voltage > req_voltage) { + // step down immediately + low_pass_voltage = req_voltage; + } else { + float delta = req_voltage - low_pass_voltage; + if (delta > 500) { + low_pass_voltage += 100; + } else { + if (delta > 50) { + low_pass_voltage += 25; + } else { + low_pass_voltage = req_voltage; + } + } + } + + if (not is_on) { + low_pass_voltage = 0.; + } + + if (last_low_pass_voltage not_eq low_pass_voltage) { + mod->serial.setOutputVoltageCurrent(low_pass_voltage, 0.); + } + + last_low_pass_voltage = low_pass_voltage; + } + }).detach(); } void power_supply_DCImpl::ready() { @@ -57,10 +93,6 @@ void power_supply_DCImpl::handle_setMode(types::power_supply_DC::Mode& mode, void power_supply_DCImpl::handle_setExportVoltageCurrent(double& voltage, double& current) { req_voltage = voltage; req_current = current; - - if (is_on) { - mod->serial.setOutputVoltageCurrent(req_voltage, req_current); - } }; void power_supply_DCImpl::handle_setImportVoltageCurrent(double& voltage, double& current){ diff --git a/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.hpp b/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.hpp index e89fdfe88..112395f8b 100644 --- a/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.hpp +++ b/modules/MicroMegaWattBSP/dc_supply/power_supply_DCImpl.hpp @@ -51,8 +51,9 @@ class power_supply_DCImpl : public power_supply_DCImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 // insert your private definitions here - float req_voltage{0}, req_current{0}; - bool is_on{false}; + std::atomic req_voltage{0}; + std::atomic req_current{0}; + std::atomic_bool is_on{false}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/OCPP/CMakeLists.txt b/modules/OCPP/CMakeLists.txt index 13ecc258c..86f4fdae7 100644 --- a/modules/OCPP/CMakeLists.txt +++ b/modules/OCPP/CMakeLists.txt @@ -18,6 +18,13 @@ target_link_libraries(${MODULE_NAME} everest::ocpp everest::ocpp_evse_security everest::ocpp_conversions + everest::external_energy_limits +) + +target_compile_options(${MODULE_NAME} + PRIVATE + -Wimplicit-fallthrough + -Werror=switch-enum ) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index f86c1b1f2..b9d8d6d9c 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -1,23 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest #include "OCPP.hpp" + +#include +#include +#include +#include + #include "generated/types/ocpp.hpp" #include "ocpp/common/types.hpp" #include "ocpp/v16/types.hpp" #include -#include #include #include #include #include -#include +#include namespace module { const std::string CERTS_SUB_DIR = "certs"; const std::string SQL_CORE_MIGRTATIONS = "core_migrations"; const std::string INOPERATIVE_ERROR_TYPE = "evse_manager/Inoperative"; +const std::string SWITCHING_PHASES_REASON = "SwitchingPhases"; namespace fs = std::filesystem; @@ -46,16 +52,44 @@ static ocpp::v16::ErrorInfo get_error_info(const Everest::error::Error& error) { "EVerest", "caused_by:" + error.message}; } - // check if is VendorError - if (error_type.find("VendorError") != std::string::npos) { - return ocpp::v16::ErrorInfo{ - uuid, ocpp::v16::ChargePointErrorCode::OtherError, false, error.message, error.origin.to_string(), - error.sub_type}; - } + const auto get_simplified_error_type = [](const std::string& error_type) { + // this function should return everything after the first '/' + // delimiter - if there is no delimiter or the delimiter is at + // the end, it should return the input itself + static constexpr auto TYPE_INTERFACE_DELIMITER = '/'; - // Default case - return ocpp::v16::ErrorInfo{ - uuid, ocpp::v16::ChargePointErrorCode::InternalError, false, error.description, std::nullopt, error_type}; + auto input = std::istringstream(error_type); + std::string tmp; + + // move right after the first delimiter + std::getline(input, tmp, TYPE_INTERFACE_DELIMITER); + + if (!input) { + // no delimiter found or delimiter at the end + return error_type; + } + + // get the rest of the input + std::getline(input, tmp); + + return tmp; + }; + + const auto is_fault = [](const Everest::error::Error&) { + // NOTE (aw): this could be customized, depending on the error + return false; + }; + + static constexpr auto TYPE_DELIMITER = '/'; + + return { + uuid, + ocpp::v16::ChargePointErrorCode::OtherError, + is_fault(error), + error.origin.to_string(), // info + error.message, // vendor id + get_simplified_error_type(error.type) + TYPE_DELIMITER + error.sub_type, // vendor error code + }; } void create_empty_user_config(const fs::path& user_config_path) { @@ -76,6 +110,13 @@ void OCPP::set_external_limits(const std::mapr_evse_energy_sink, connector_id)) { + EVLOG_warning << "Can not apply external limits! No evse energy sink configured for evse_id: " + << connector_id; + continue; + } + types::energy::ExternalLimits limits; std::vector schedule_import; for (const auto period : schedule.chargingSchedulePeriod) { @@ -83,11 +124,11 @@ void OCPP::set_external_limits(const std::mapr_connector_zero_sink.empty()) { - EVLOG_debug << "OCPP sets the following external limits for connector 0: \n" << limits; - this->r_connector_zero_sink.at(0)->call_set_external_limits(limits); - } else { - EVLOG_debug << "OCPP cannot set external limits for connector 0. No " - "sink is configured."; - } - } else { - EVLOG_debug << "OCPP sets the following external limits for connector " << connector_id << ": \n" << limits; - this->r_evse_manager.at(connector_id - 1)->call_set_external_limits(limits); - } + auto& evse_sink = external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, connector_id); + evse_sink.call_set_external_limits(limits); } } @@ -141,7 +171,7 @@ void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::Ses << "Received TransactionStarted"; const auto transaction_started = session_event.transaction_started.value(); - const auto timestamp = ocpp::DateTime(session_event.timestamp); + const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp); const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total; const auto session_id = session_event.uuid; const auto id_token = transaction_started.id_tag.id_token.value; @@ -167,6 +197,10 @@ void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::Ses EVLOG_debug << "Connector#" << ocpp_connector_id << ": " << "Received ChargingPausedEVSE"; this->charge_point->on_suspend_charging_evse(ocpp_connector_id); + } else if (session_event.event == types::evse_manager::SessionEventEnum::SwitchingPhases) { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received SwitchingPhases"; + this->charge_point->on_suspend_charging_evse(ocpp_connector_id, SWITCHING_PHASES_REASON); } else if (session_event.event == types::evse_manager::SessionEventEnum::ChargingStarted || session_event.event == types::evse_manager::SessionEventEnum::ChargingResumed) { EVLOG_debug << "Connector#" << ocpp_connector_id << ": " @@ -177,7 +211,7 @@ void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::Ses << "Received TransactionFinished"; const auto transaction_finished = session_event.transaction_finished.value(); - const auto timestamp = ocpp::DateTime(session_event.timestamp); + const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp); const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total; const auto signed_meter_value = transaction_finished.signed_meter_value; @@ -231,6 +265,9 @@ void OCPP::init_evse_subscriptions() { // soc is present, so add this to the measurement measurement.soc_Percent = ocpp::StateOfCharge{this->evse_soc_map[evse_id].value()}; } + if (powermeter.temperatures.has_value()) { + measurement.temperature_C = conversions::to_ocpp_temperatures(powermeter.temperatures.value()); + } this->charge_point->on_meter_values(evse_id, measurement); }); @@ -325,6 +362,16 @@ bool OCPP::all_evse_ready() { return true; } +ocpp::v16::ChargingRateUnit get_unit_or_default(const std::string& unit_string) { + try { + return ocpp::v16::conversions::string_to_charging_rate_unit(unit_string); + } catch (const std::out_of_range& e) { + EVLOG_warning << "RequestCompositeScheduleUnit configured incorrectly with: " << unit_string + << ". Defaulting to using Amps."; + return ocpp::v16::ChargingRateUnit::A; + } +} + void OCPP::init() { invoke_init(*p_main); invoke_init(*p_ocpp_generic); @@ -332,6 +379,16 @@ void OCPP::init() { invoke_init(*p_auth_provider); invoke_init(*p_data_transfer); + // ensure all evse_energy_sink(s) that are connected have an evse id mapping + for (const auto& evse_sink : this->r_evse_energy_sink) { + if (not evse_sink->get_mapping().has_value()) { + EVLOG_critical << "Please configure an evse mapping your configuration file for the connected " + "r_evse_energy_sink with module_id: " + << evse_sink->module_id; + throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse."); + } + } + const auto error_handler = [this](const Everest::error::Error& error) { const auto evse_id = error.origin.mapping.has_value() ? error.origin.mapping.value().evse : 0; const auto error_info = get_error_info(error); @@ -497,10 +554,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); }); @@ -577,10 +642,7 @@ void OCPP::ready() { firmware_update_request.location = msg.firmware.location; firmware_update_request.signature.emplace(msg.firmware.signature.get()); firmware_update_request.signing_certificate.emplace(msg.firmware.signingCertificate.get()); - - if (msg.firmware.retrieveDateTime.has_value()) { - firmware_update_request.retrieve_timestamp.emplace(msg.firmware.retrieveDateTime.value().to_rfc3339()); - } + firmware_update_request.retrieve_timestamp.emplace(msg.firmware.retrieveDateTime.to_rfc3339()); if (msg.firmware.installDateTime.has_value()) { firmware_update_request.install_timestamp.emplace(msg.firmware.installDateTime.value()); @@ -664,15 +726,29 @@ 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 const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules( - this->config.PublishChargingScheduleDurationS); + this->config.PublishChargingScheduleDurationS, composite_schedule_unit); this->set_external_limits(charging_schedules); this->publish_charging_schedules(charging_schedules); - this->charging_schedules_timer = std::make_unique([this]() { + this->charging_schedules_timer = std::make_unique([this, composite_schedule_unit]() { const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules( - this->config.PublishChargingScheduleDurationS); + this->config.PublishChargingScheduleDurationS, composite_schedule_unit); this->set_external_limits(charging_schedules); this->publish_charging_schedules(charging_schedules); }); @@ -680,12 +756,12 @@ void OCPP::ready() { this->charging_schedules_timer->interval(std::chrono::seconds(this->config.PublishChargingScheduleIntervalS)); } - this->charge_point->register_signal_set_charging_profiles_callback([this]() { + this->charge_point->register_signal_set_charging_profiles_callback([this, composite_schedule_unit]() { // this is executed when CSMS sends new ChargingProfile that is accepted by // the ChargePoint EVLOG_info << "Received new Charging Schedules from CSMS"; const auto charging_schedules = this->charge_point->get_all_enhanced_composite_charging_schedules( - this->config.PublishChargingScheduleDurationS); + this->config.PublishChargingScheduleDurationS, composite_schedule_unit); this->set_external_limits(charging_schedules); this->publish_charging_schedules(charging_schedules); }); @@ -708,8 +784,12 @@ void OCPP::ready() { const ocpp::v201::CertificateActionEnum& certificate_action) { types::iso15118_charger::ResponseExiStreamStatus response; response.status = conversions::to_everest_iso15118_charger_status(certificate_response.status); - response.exi_response.emplace(certificate_response.exiResponse.get()); response.certificate_action = conversions::to_everest_certificate_action_enum(certificate_action); + if (not certificate_response.exiResponse.get().empty()) { + // since exi_response is an optional in the EVerest type we only set it when not empty + response.exi_response.emplace(certificate_response.exiResponse.get()); + } + this->r_evse_manager.at(this->connector_evse_index_map.at(connector_id)) ->call_set_get_certificate_response(response); }); @@ -724,25 +804,32 @@ void OCPP::ready() { this->charge_point->register_transaction_started_callback( [this](const int32_t connector, const std::string& session_id) { - types::ocpp::OcppTransactionEvent tevent = { - types::ocpp::TransactionEvent::Started, connector, 1, session_id, std::nullopt, - }; + types::ocpp::OcppTransactionEvent tevent; + tevent.transaction_event = types::ocpp::TransactionEvent::Started; + tevent.evse = {connector, 1}; + tevent.session_id = session_id; p_ocpp_generic->publish_ocpp_transaction_event(tevent); }); this->charge_point->register_transaction_updated_callback( [this](const int32_t connector, const std::string& session_id, const int32_t transaction_id, const ocpp::v16::IdTagInfo& id_tag_info) { - types::ocpp::OcppTransactionEvent tevent = {types::ocpp::TransactionEvent::Updated, connector, 1, - session_id, std::to_string(transaction_id)}; + types::ocpp::OcppTransactionEvent tevent; + tevent.transaction_event = types::ocpp::TransactionEvent::Updated; + tevent.evse = {connector, 1}; + tevent.session_id = session_id; + tevent.transaction_id = std::to_string(transaction_id); p_ocpp_generic->publish_ocpp_transaction_event(tevent); }); this->charge_point->register_transaction_stopped_callback( [this](const int32_t connector, const std::string& session_id, const int32_t transaction_id) { EVLOG_info << "Transaction stopped at connector: " << connector << "session_id: " << session_id; - types::ocpp::OcppTransactionEvent tevent = {types::ocpp::TransactionEvent::Ended, connector, 1, session_id, - std::to_string(transaction_id)}; + types::ocpp::OcppTransactionEvent tevent; + tevent.transaction_event = types::ocpp::TransactionEvent::Ended; + tevent.evse = {connector, 1}; + tevent.session_id = session_id; + tevent.transaction_id = std::to_string(transaction_id); p_ocpp_generic->publish_ocpp_transaction_event(tevent); }); diff --git a/modules/OCPP/OCPP.hpp b/modules/OCPP/OCPP.hpp index deb467907..05df5f132 100644 --- a/modules/OCPP/OCPP.hpp +++ b/modules/OCPP/OCPP.hpp @@ -63,6 +63,7 @@ struct Conf { int PublishChargingScheduleDurationS; std::string MessageLogPath; int MessageQueueResumeDelay; + std::string RequestCompositeScheduleUnit; }; class OCPP : public Everest::ModuleBase { @@ -75,7 +76,7 @@ class OCPP : public Everest::ModuleBase { std::unique_ptr p_data_transfer, std::unique_ptr p_ocpp_generic, std::unique_ptr p_session_cost, std::vector> r_evse_manager, - std::vector> r_connector_zero_sink, + std::vector> r_evse_energy_sink, std::unique_ptr r_reservation, std::unique_ptr r_auth, std::unique_ptr r_system, std::unique_ptr r_security, std::vector> r_data_transfer, @@ -89,14 +90,15 @@ class OCPP : public Everest::ModuleBase { p_ocpp_generic(std::move(p_ocpp_generic)), p_session_cost(std::move(p_session_cost)), r_evse_manager(std::move(r_evse_manager)), - r_connector_zero_sink(std::move(r_connector_zero_sink)), + r_evse_energy_sink(std::move(r_evse_energy_sink)), r_reservation(std::move(r_reservation)), r_auth(std::move(r_auth)), r_system(std::move(r_system)), 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; @@ -106,7 +108,7 @@ class OCPP : public Everest::ModuleBase { const std::unique_ptr p_ocpp_generic; const std::unique_ptr p_session_cost; const std::vector> r_evse_manager; - const std::vector> r_connector_zero_sink; + const std::vector> r_evse_energy_sink; const std::unique_ptr r_reservation; const std::unique_ptr r_auth; const std::unique_ptr r_system; @@ -138,6 +140,7 @@ class OCPP : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here std::filesystem::path ocpp_share_path; + ocpp::v16::ChargingRateUnit composite_schedule_charging_rate_unit; void set_external_limits(const std::map& charging_schedules); void publish_charging_schedules(const std::map& charging_schedules); @@ -162,6 +165,7 @@ class OCPP : public Everest::ModuleBase { }; // ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here // ev@087e516b-124c-48df-94fb-109508c7cda9:v1 } // namespace module 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/conversions.cpp b/modules/OCPP/conversions.cpp index d2903c24e..07f61a099 100644 --- a/modules/OCPP/conversions.cpp +++ b/modules/OCPP/conversions.cpp @@ -38,9 +38,8 @@ to_ocpp_firmware_status_notification(const types::system::FirmwareUpdateStatusEn return ocpp::FirmwareStatusNotification::InvalidSignature; case types::system::FirmwareUpdateStatusEnum::SignatureVerified: return ocpp::FirmwareStatusNotification::SignatureVerified; - default: - throw std::out_of_range("Could not convert FirmwareUpdateStatusEnum to FirmwareStatusNotification"); } + throw std::out_of_range("Could not convert FirmwareUpdateStatusEnum to FirmwareStatusNotification"); } ocpp::SessionStartedReason to_ocpp_session_started_reason(const types::evse_manager::StartSessionReason reason) { @@ -49,10 +48,8 @@ ocpp::SessionStartedReason to_ocpp_session_started_reason(const types::evse_mana return ocpp::SessionStartedReason::EVConnected; case types::evse_manager::StartSessionReason::Authorized: return ocpp::SessionStartedReason::Authorized; - default: - throw std::out_of_range( - "Could not convert types::evse_manager::StartSessionReason to ocpp::SessionStartedReason"); } + throw std::out_of_range("Could not convert types::evse_manager::StartSessionReason to ocpp::SessionStartedReason"); } ocpp::v16::DataTransferStatus to_ocpp_data_transfer_status(const types::ocpp::DataTransferStatus status) { @@ -65,9 +62,10 @@ ocpp::v16::DataTransferStatus to_ocpp_data_transfer_status(const types::ocpp::Da return ocpp::v16::DataTransferStatus::UnknownMessageId; case types::ocpp::DataTransferStatus::UnknownVendorId: return ocpp::v16::DataTransferStatus::UnknownVendorId; - default: + case types::ocpp::DataTransferStatus::Offline: return ocpp::v16::DataTransferStatus::UnknownVendorId; } + return ocpp::v16::DataTransferStatus::UnknownVendorId; } ocpp::v16::Reason to_ocpp_reason(const types::evse_manager::StopTransactionReason reason) { @@ -104,9 +102,8 @@ ocpp::v16::Reason to_ocpp_reason(const types::evse_manager::StopTransactionReaso case types::evse_manager::StopTransactionReason::Timeout: case types::evse_manager::StopTransactionReason::Other: return ocpp::v16::Reason::Other; - default: - throw std::out_of_range("Could not convert types::evse_manager::StopTransactionReason to ocpp::v16::Reason"); } + throw std::out_of_range("Could not convert types::evse_manager::StopTransactionReason to ocpp::v16::Reason"); } ocpp::v201::CertificateActionEnum @@ -116,10 +113,9 @@ to_ocpp_certificate_action_enum(const types::iso15118_charger::CertificateAction return ocpp::v201::CertificateActionEnum::Install; case types::iso15118_charger::CertificateActionEnum::Update: return ocpp::v201::CertificateActionEnum::Update; - default: - throw std::out_of_range( - "Could not convert types::iso15118_charger::CertificateActionEnum to ocpp::v201::CertificateActionEnum"); } + throw std::out_of_range( + "Could not convert types::iso15118_charger::CertificateActionEnum to ocpp::v201::CertificateActionEnum"); } ocpp::v16::ReservationStatus to_ocpp_reservation_status(const types::reservation::ReservationResult result) { @@ -134,10 +130,8 @@ ocpp::v16::ReservationStatus to_ocpp_reservation_status(const types::reservation return ocpp::v16::ReservationStatus::Rejected; case types::reservation::ReservationResult::Unavailable: return ocpp::v16::ReservationStatus::Unavailable; - default: - throw std::out_of_range( - "Could not convert types::reservation::ReservationResult to ocpp::v16::ReservationStatus"); } + throw std::out_of_range("Could not convert types::reservation::ReservationResult to ocpp::v16::ReservationStatus"); } ocpp::v16::LogStatusEnumType to_ocpp_log_status_enum_type(const types::system::UploadLogsStatus status) { @@ -148,9 +142,8 @@ ocpp::v16::LogStatusEnumType to_ocpp_log_status_enum_type(const types::system::U return ocpp::v16::LogStatusEnumType::Rejected; case types::system::UploadLogsStatus::AcceptedCanceled: return ocpp::v16::LogStatusEnumType::AcceptedCanceled; - default: - throw std::out_of_range("Could not convert types::system::UploadLogsStatus to ocpp::v16::LogStatusEnumType"); } + throw std::out_of_range("Could not convert types::system::UploadLogsStatus to ocpp::v16::LogStatusEnumType"); } ocpp::v16::UpdateFirmwareStatusEnumType @@ -164,10 +157,11 @@ to_ocpp_update_firmware_status_enum_type(const types::system::UpdateFirmwareResp return ocpp::v16::UpdateFirmwareStatusEnumType::AcceptedCanceled; case types::system::UpdateFirmwareResponse::InvalidCertificate: return ocpp::v16::UpdateFirmwareStatusEnumType::InvalidCertificate; - default: - throw std::out_of_range( - "Could not convert types::system::UpdateFirmwareResponse to ocpp::v16::UpdateFirmwareStatusEnumType"); + case types::system::UpdateFirmwareResponse::RevokedCertificate: + return ocpp::v16::UpdateFirmwareStatusEnumType::RevokedCertificate; } + throw std::out_of_range( + "Could not convert types::system::UpdateFirmwareResponse to ocpp::v16::UpdateFirmwareStatusEnumType"); } ocpp::v16::HashAlgorithmEnumType @@ -179,10 +173,9 @@ to_ocpp_hash_algorithm_enum_type(const types::iso15118_charger::HashAlgorithm ha return ocpp::v16::HashAlgorithmEnumType::SHA384; case types::iso15118_charger::HashAlgorithm::SHA512: return ocpp::v16::HashAlgorithmEnumType::SHA512; - default: - throw std::out_of_range( - "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v16::HashAlgorithmEnumType"); } + throw std::out_of_range( + "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v16::HashAlgorithmEnumType"); } ocpp::v16::BootReasonEnum to_ocpp_boot_reason_enum(const types::system::BootReason reason) { @@ -205,14 +198,13 @@ ocpp::v16::BootReasonEnum to_ocpp_boot_reason_enum(const types::system::BootReas return ocpp::v16::BootReasonEnum::Unknown; case types::system::BootReason::Watchdog: return ocpp::v16::BootReasonEnum::Watchdog; - default: - throw std::runtime_error("Could not convert BootReasonEnum"); } + throw std::runtime_error("Could not convert BootReasonEnum"); } ocpp::Powermeter to_ocpp_power_meter(const types::powermeter::Powermeter& powermeter) { ocpp::Powermeter ocpp_powermeter; - ocpp_powermeter.timestamp = powermeter.timestamp; + ocpp_powermeter.timestamp = ocpp_conversions::to_ocpp_datetime_or_now(powermeter.timestamp); ocpp_powermeter.energy_Wh_import = {powermeter.energy_Wh_import.total, powermeter.energy_Wh_import.L1, powermeter.energy_Wh_import.L2, powermeter.energy_Wh_import.L3}; @@ -253,6 +245,19 @@ ocpp::Powermeter to_ocpp_power_meter(const types::powermeter::Powermeter& powerm return ocpp_powermeter; } +std::vector to_ocpp_temperatures(const std::vector& temperatures) { + std::vector ocpp_temperatures; + for (const auto temperature : temperatures) { + ocpp::Temperature ocpp_temperature; + ocpp_temperature.value = temperature.temperature; + if (temperature.location.has_value()) { + ocpp_temperature.location = temperature.location.value(); + } + ocpp_temperatures.push_back(ocpp_temperature); + } + return ocpp_temperatures; +} + ocpp::v201::HashAlgorithmEnum to_ocpp_hash_algorithm_enum(const types::iso15118_charger::HashAlgorithm hash_algorithm) { switch (hash_algorithm) { case types::iso15118_charger::HashAlgorithm::SHA256: @@ -261,10 +266,9 @@ ocpp::v201::HashAlgorithmEnum to_ocpp_hash_algorithm_enum(const types::iso15118_ return ocpp::v201::HashAlgorithmEnum::SHA384; case types::iso15118_charger::HashAlgorithm::SHA512: return ocpp::v201::HashAlgorithmEnum::SHA512; - default: - throw std::out_of_range( - "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v16::HashAlgorithmEnumType"); } + throw std::out_of_range( + "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v16::HashAlgorithmEnumType"); } types::evse_manager::StopTransactionReason to_everest_stop_transaction_reason(const ocpp::v16::Reason reason) { @@ -291,9 +295,8 @@ types::evse_manager::StopTransactionReason to_everest_stop_transaction_reason(co return types::evse_manager::StopTransactionReason::DeAuthorized; case ocpp::v16::Reason::Other: return types::evse_manager::StopTransactionReason::Other; - default: - throw std::out_of_range("Could not convert ocpp::v16::Reason to types::evse_manager::StopTransactionReason"); } + throw std::out_of_range("Could not convert ocpp::v16::Reason to types::evse_manager::StopTransactionReason"); } types::system::ResetType to_everest_reset_type(const ocpp::v16::ResetType type) { @@ -302,9 +305,8 @@ types::system::ResetType to_everest_reset_type(const ocpp::v16::ResetType type) return types::system::ResetType::Hard; case ocpp::v16::ResetType::Soft: return types::system::ResetType::Soft; - default: - throw std::out_of_range("Could not convert ocpp::v16::ResetType to types::system::ResetType"); } + throw std::out_of_range("Could not convert ocpp::v16::ResetType to types::system::ResetType"); } types::iso15118_charger::Status @@ -314,10 +316,9 @@ to_everest_iso15118_charger_status(const ocpp::v201::Iso15118EVCertificateStatus return types::iso15118_charger::Status::Accepted; case ocpp::v201::Iso15118EVCertificateStatusEnum::Failed: return types::iso15118_charger::Status::Failed; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::Iso15118EVCertificateStatusEnum to types::iso15118_charger::Status"); } + throw std::out_of_range( + "Could not convert ocpp::v201::Iso15118EVCertificateStatusEnum to types::iso15118_charger::Status"); } types::iso15118_charger::CertificateActionEnum @@ -327,10 +328,9 @@ to_everest_certificate_action_enum(const ocpp::v201::CertificateActionEnum actio return types::iso15118_charger::CertificateActionEnum::Install; case ocpp::v201::CertificateActionEnum::Update: return types::iso15118_charger::CertificateActionEnum::Update; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::CertificateActionEnum to types::iso15118_charger::CertificateActionEnum"); } + throw std::out_of_range( + "Could not convert ocpp::v201::CertificateActionEnum to types::iso15118_charger::CertificateActionEnum"); } types::authorization::CertificateStatus @@ -350,10 +350,9 @@ to_everest_certificate_status(const ocpp::v201::AuthorizeCertificateStatusEnum s return types::authorization::CertificateStatus::CertChainError; case ocpp::v201::AuthorizeCertificateStatusEnum::ContractCancelled: return types::authorization::CertificateStatus::ContractCancelled; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::AuthorizeCertificateStatusEnum to types::authorization::CertificateStatus"); } + throw std::out_of_range( + "Could not convert ocpp::v201::AuthorizeCertificateStatusEnum to types::authorization::CertificateStatus"); } types::authorization::AuthorizationStatus to_everest_authorization_status(const ocpp::v16::AuthorizationStatus status) { @@ -368,10 +367,9 @@ types::authorization::AuthorizationStatus to_everest_authorization_status(const return types::authorization::AuthorizationStatus::Invalid; case ocpp::v16::AuthorizationStatus::ConcurrentTx: return types::authorization::AuthorizationStatus::ConcurrentTx; - default: - throw std::out_of_range( - "Could not convert ocpp::v16::AuthorizationStatus to types::authorization::AuthorizationStatus"); } + throw std::out_of_range( + "Could not convert ocpp::v16::AuthorizationStatus to types::authorization::AuthorizationStatus"); } types::authorization::AuthorizationStatus @@ -397,20 +395,18 @@ to_everest_authorization_status(const ocpp::v201::AuthorizationStatusEnum status return types::authorization::AuthorizationStatus::NotAtThisTime; case ocpp::v201::AuthorizationStatusEnum::Unknown: return types::authorization::AuthorizationStatus::Unknown; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::AuthorizationStatusEnum to types::authorization::AuthorizationStatus"); } + throw std::out_of_range( + "Could not convert ocpp::v201::AuthorizationStatusEnum to types::authorization::AuthorizationStatus"); } types::ocpp::ChargingSchedulePeriod to_charging_schedule_period(const ocpp::v16::EnhancedChargingSchedulePeriod& period) { - types::ocpp::ChargingSchedulePeriod csp = { - period.startPeriod, - period.limit, - period.stackLevel, - period.numberPhases, - }; + types::ocpp::ChargingSchedulePeriod csp; + csp.start_period = period.startPeriod; + csp.limit = period.limit; + csp.number_phases = period.numberPhases; + csp.stack_level = period.stackLevel; return csp; } @@ -449,37 +445,34 @@ to_everest_registration_status(const ocpp::v16::RegistrationStatus& registration return types::ocpp::RegistrationStatus::Pending; case ocpp::v16::RegistrationStatus::Rejected: return types::ocpp::RegistrationStatus::Rejected; - default: - throw std::out_of_range("Could not convert ocpp::v201::RegistrationStatus to types::ocpp::RegistrationStatus"); } + throw std::out_of_range("Could not convert ocpp::v201::RegistrationStatus to types::ocpp::RegistrationStatus"); } -ocpp::v16::DataTransferResponse -to_ocpp_data_transfer_response(const types::display_message::SetDisplayMessageResponse& set_display_message_response) { - ocpp::v16::DataTransferResponse response; - switch (set_display_message_response.status) { +ocpp::v16::DataTransferStatus +to_ocpp_data_transfer_status(const types::display_message::DisplayMessageStatusEnum display_message_status_enum) { + switch (display_message_status_enum) { case types::display_message::DisplayMessageStatusEnum::Accepted: - response.status = ocpp::v16::DataTransferStatus::Accepted; - break; + return ocpp::v16::DataTransferStatus::Accepted; case types::display_message::DisplayMessageStatusEnum::NotSupportedMessageFormat: - response.status = ocpp::v16::DataTransferStatus::Rejected; - break; + return ocpp::v16::DataTransferStatus::Rejected; case types::display_message::DisplayMessageStatusEnum::Rejected: - response.status = ocpp::v16::DataTransferStatus::Rejected; - break; + return ocpp::v16::DataTransferStatus::Rejected; case types::display_message::DisplayMessageStatusEnum::NotSupportedPriority: - response.status = ocpp::v16::DataTransferStatus::Rejected; - break; + return ocpp::v16::DataTransferStatus::Rejected; case types::display_message::DisplayMessageStatusEnum::NotSupportedState: - response.status = ocpp::v16::DataTransferStatus::Rejected; - break; + return ocpp::v16::DataTransferStatus::Rejected; case types::display_message::DisplayMessageStatusEnum::UnknownTransaction: - response.status = ocpp::v16::DataTransferStatus::Rejected; - break; - default: - throw std::out_of_range( - "Could not convert types::display_message::DisplayMessageStatusEnum to ocpp::v16::DataTransferStatus"); + return ocpp::v16::DataTransferStatus::Rejected; } + throw std::out_of_range( + "Could not convert types::display_message::DisplayMessageStatusEnum to ocpp::v16::DataTransferStatus"); +} + +ocpp::v16::DataTransferResponse +to_ocpp_data_transfer_response(const types::display_message::SetDisplayMessageResponse& set_display_message_response) { + ocpp::v16::DataTransferResponse response; + response.status = to_ocpp_data_transfer_status(set_display_message_response.status); response.data = set_display_message_response.status_info; return response; diff --git a/modules/OCPP/conversions.hpp b/modules/OCPP/conversions.hpp index 108fa5499..416bfb315 100644 --- a/modules/OCPP/conversions.hpp +++ b/modules/OCPP/conversions.hpp @@ -63,6 +63,9 @@ ocpp::v16::BootReasonEnum to_ocpp_boot_reason_enum(const types::system::BootReas /// \brief Converts a given types::powermeter::Powermeter \p powermeter to a ocpp::Powermeter ocpp::Powermeter to_ocpp_power_meter(const types::powermeter::Powermeter& powermeter); +/// \brief Converts a given vector of types::temperature::Temperature \p powermeter to a vector of ocpp::Temperature +std::vector to_ocpp_temperatures(const std::vector& temperatures); + /// \brief Converts a given types::iso15118_charger::HashAlgorithm \p hash_algorithm to a ocpp::v201::HashAlgorithmEnum. ocpp::v201::HashAlgorithmEnum to_ocpp_hash_algorithm_enum(const types::iso15118_charger::HashAlgorithm hash_algorithm); diff --git a/modules/OCPP/doc.rst b/modules/OCPP/doc.rst index b1fbce819..aa038523e 100644 --- a/modules/OCPP/doc.rst +++ b/modules/OCPP/doc.rst @@ -1,41 +1,385 @@ +.. _everest_modules_handwritten_OCPP: + +OCPP1.6 Module +============== + +This module implements and integrates OCPP1.6 within EVerest, including all feature profiles defined by the specification. A connection +to a Charging Station Management System (CSMS) can be established by loading this module as part of the EVerest configuration. This module +leverages `libocpp `_, EVerest's OCPP library. + +The EVerest config `config-sil-ocpp.yaml <../../config/config-sil-ocpp.yaml>`_ serves as an example for how to add the OCPP module to +your EVerest config. + +Module configuration +-------------------- + +Like for every EVerest module, the configuration parameters are defined as part of the module `manifest `_. This module +is a little special though. The OCPP1.6 protocol defines a lot of standardized configuration keys that are used as part of the functional +requirements of the specification. These configuration keys mainly influence the control flow of libocpp and are managed by a separate +JSON configuration file. The module uses the configuration parameter **ChargePointConfigPath** to point to this file. + +`This EVerest OCPP tutorial `_, the OCPP specification, and +`libocpp's documentation `_ are great resources to learn about the different configuration options. + +Integration in EVerest +---------------------- + +This module leverages `libocpp `_, EVerest's OCPP library. Libocpp's approach to implementing the +OCPP protocol is to do as much work as possible as part of the library. It therefore fulfills a large amount of protocol requirements +internally. OCPP is a protocol that affects, controls, and monitors many areas of a charging station's operation, though. It is therefore +required to integrate libocpp with other parts of EVerest. This integration is done by this module and will be explained in this section. + +For a detailed description of libocpp and its functionalities, please refer to `its documentation `_. + +The `manifest `_ of this module defines requirements and implementations of EVerest interfaces to integrate the OCPP +communication with other parts of EVerest. In order to describe how the responsibilities for functions and operations required by OCPP +are divided between libocpp and this module, the following sections pick up the requirements of this module and implementations one by one. + +Provides: main +^^^^^^^^^^^^^^ + +**Interface**: `ocpp_1_6_charge_point <../interfaces/ocpp_1_6_charge_point.yaml>`_ + +This interface is implemented to provide an API to control the websocket connection and to control and retrieve OCPP-specific data like +security events and configuration keys. + +*Note: This interface is deprecated soon and will be removed soon. The functionality is already covered by the generic interface* +`ocpp <../interfaces/ocpp.yaml>`_ *which is used by this module and OCPP201.* + +Provides: auth_validator +^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `auth_token_validator <../interfaces/auth_token_validator.yaml>`_ + +This interface is implemented to forward authorization requests from EVerest to libocpp. Libocpp contains the business logic to either +validate the authorization request locally using the authorization cache and local authorization list or to forward the request to the +CSMS using an **Authorize.req**. The implementation also covers the validation of Plug&Charge authorization requests by triggering a +`DataTransfer.req(Authorize)`. + +Provides: auth_provider +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `auth_token_provider <../../interfaces/auth_token_provider.yaml>`_ + +This interface is implemented to publish authorization requests from the CSMS within EVerest. An authorization request from the CSMS is +turned out by a **RemoteStartTransaction.req**. + +Provides: data_transfer +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp_data_transfer <../../interfaces/ocpp_data_transfer.yaml>`_ + +This interface is implemented to provide a command to initiate a **DataTransfer.req** from the charging station to the CSMS. + +Provides: ocpp_generic +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp <../../interfaces/ocpp.yaml>`_ + +This interface is implemented to provide an API to control an OCPP service and to set and get OCPP-specific data. + +Provides: session_cost +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `session_cost <../../interfaces/session_cost.yaml>`_ + +This interface is implemented to publish session costs received by the CSMS as part of the California Pricing whitepaper extension. + +Requires: evse_manager +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `evse_manager <../../interfaces/evse_manager.yaml>`_ + +Typically the `EvseManager <../EvseManager/>`_ module is used to fulfill this requirement. + +This module requires (1-128) implementations of this interface in order to integrate with the charge control logic of EVerest. One +connection represents one EVSE. In order to manage multiple EVSEs via one OCPP connection, multiple connections need to be configured +in the EVerest config file. + +This module makes use of the following commands of this interface: + +* **get_evse** to get the EVSE id of the module implementing the **evse_manager** interface at startup +* **pause_charging** to pause charging in case a **StopTransaction.conf** indicates charging shall be paused +* **resume_charging** to resume charging +* **stop_transaction** to stop a transaction in case the CSMS stops a transaction by e.g. a **RemoteStopTransaction.req** +* **force_unlock** to force the unlock of a connector in case the CSMS sends a **UnlockConnector.req** +* **enable_disable** to set the EVSE to operative or inoperative, e.g., in case the CSMS sends a **ChangeAvailability.req**. This command can + be called from different sources. It therefore contains an argument **priority** in order to override the status if required. OCPP uses + a priority of 5000, which is mid-range. +* **set_external_limits** to apply a power or ampere limits at the EVSE received by the CSMS using the SmartCharging feature profile. + Libocpp contains the business logic to calculate the composite schedule for received charging profiles. This module gets notified in + case charging profiles are added, changed, or cleared. When notified, this module requests the composite schedule from libocpp and + publishes the result via the `Provides: ocpp_generic <#provides-ocpp_generic>`_ interface. The duration of the composite schedule can + be configured by the configuration parameter **PublishChargingScheduleDurationS** of this module. The configuration parameter + **PublishChargingScheduleIntervalS** defines the interval to use to periodically retrieve and publish the composite schedules. +* **set_get_certificate_response** to report that the charging station received a **DataTransfer.conf(Get15118EVCertificateResponse)** from + the CSMS (EV Contract installation for Plug&Charge) +* **external_ready_to_start_charging**: To signal that the module has started to establish an OCPP connection to the CSMS + +The interface is used to receive the following variables: + +* **powermeter** to push powermeter values of an EVSE. Libocpp initiates **MeterValues.req** internally and is responsible to comply with + the configured intervals and measurands for clock-aligned and sampled meter values. +* **ev_info** to obtain the state of charge (SoC) of an EV. If present, this is reported as part of a **MeterValues.req** +* **limits** to obtain the current offered to the EV. If present, this is reported as part of a **MeterValues.req** +* **session_event** to trigger **StatusNotification.req**, **StartTransaction.req**, and **StopTransaction.req** based on the reported event. + This signal drives the state machine and the transaction handling of libocpp. +* **iso15118_certificate_request** to trigger a **DataTransfer.req(Get15118EVCertificateRequest)** as part of the Plug&Charge process +* **waiting_for_external_ready** to obtain the information that a module implementing this interface is waiting for an external ready signal +* **ready** to obtain a ready signal from a module implementing this interface + +Requires: connector_zero_sink +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `external_energy_limits <../../interfaces/external_energy_limits.yaml>`_ + +Typically the `EnergyNode <../EnergyNode/>`_ module is used to fulfill this requirement. + +This module optionally requires the connection to a module implementing the **external_energy_limits** interface. This connection is used +to apply power or ampere limits at EVSE id zero received by the CSMS using the SmartCharging feature profile. + +This module makes use of the following commands of this interface: + +* **set_external_limits** to apply a power or ampere limits for EVSE id zero (the whole charging station). + +Requires: reservation +^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `reservation <../../interfaces/reservation.yaml>`_ + +Typically the `Auth <../Auth/>`_ module is used to fulfill this requirement. + +This module requires a connection to a module implementing the `reservation` interface. This connection is used to apply reservation +requests from the CSMS. + +This module makes use of the following commands of this interface: + +* **reserve_now** which is called when the CSMS sends a **ReserveNow.req** +* **cancel_reservation** which is called when the CSMS sends a **CancelReservation.req** + +Requires: auth +^^^^^^^^^^^^^^ + +**Interface**: `auth <../../interfaces/auth.yaml>`_ + +Typically the `Auth <../Auth/>`_ module is used to fulfill this requirement. + +This module requires a connection to a module implementing the **auth** interface. This connection is used to set the standardized +**ConnectionTimeout** configuration key if configured and/or changed by the CSMS. + +This module makes use of the following commands of this interface: + +* **set_connection_timeout** which is e.g., called in case the CSMS uses a **ChangeConfiguration.req(ConnectionTimeout)** + +Requires: system +^^^^^^^^^^^^^^^^ + +**Interface**: `system <../../interfaces/system.yaml>`_ + +The `System <../System/>`_ module can be used to fulfill this requirement. Note that this module is not meant to be used in +production systems without any modification! + +This module requires a connection to a module implementing the **system** interface. This connection is used to execute and control +system-wide operations that can be triggered by the CSMS, like log uploads, firmware updates, and resets. + +This module makes use of the following commands of this interface: + +* **update_firmware** to forward **UpdateFirmware.req** or **SignedUpdateFirmware.req** messages from the CSMS +* **allow_firmware_installation** to notify the module that the installation of the firmware is now allowed. A prerequisite for this + is that all EVSEs are set to inoperative. This module and libocpp take care of setting the EVSEs to inoperative before calling + this command. +* **upload_logs** to forward **GetDiagnostics.req** or **GetLog.req** messages from the CSMS +* **is_reset_allowed** to check if a **Reset.req** message from the CSMS shall be accepted or rejected +* **reset** to perform a reset in case of a **Reset.req** message from the CSMS +* **set_system_time** to set the system time communicated by a **BootNotification.conf** or **Heartbeat.conf** messages from the CSMS +* **get_boot_reason** to obtain the boot reason to use it as part of the **BootNotification.req** at startup + +The interface is used to receive the following variables: + +* **log_status** to obtain the log update status. This triggers a **LogStatusNotification.req** or **DiagnosticsStatusNotification.req** + message to inform the CSMS about the current status. This signal is expected as a result of an **upload_logs** command. +* **firmware_update_status** to obtain the firmware update status. This triggers a **FirmwareStatusNotification.req** or + **SignedFirmwareStatusNotification.req** message to inform the CSMS about the current status. This signal is expected as a result + of an **update_firmware** command. + +Requires: security +^^^^^^^^^^^^^^^^^^ + +**Interface**: `evse_security <../../interfaces/evse_security.yaml>`_ + +This module requires a connection to a module implementing the `evse_security` interface. This connection is used to execute +security-related operations and to manage certificates and private keys. + +Typically the `EvseSecurity <../EvseSecurity/>`_ module is used to fulfill this requirement. + +This module makes use of the following commands of this interface: + +* **install_ca_certificate** to handle **InstallCertificate.req** and **DataTransfer.req(InstallCertificate) messages from the CSMS +* **delete_certificate** to handle **DeleteCertificate.req** and **DataTransfer.req(DeleteCertificate)** messages from the CSMS +* **update_leaf_certificate** to handle **CertificateSigned.req** and **DataTransfer.req(CertificateSigned) messages from the CSMS +* **verify_certificate** to verify certificates from the CSMS that are sent as part of **SignedUpdateFirmware.req** +* **get_installed_certificates** to handle **GetInstalledCertificateIds.req** and **DataTransfer.req(GetInstalledCertificateIds)** + messages from the CSMS +* **get_v2g_ocsp_request_data** to update the OCSP cache of V2G sub-CA certificates using a **DataTransfer.req(GetCertificateStatus)**. + Triggering this message is handled by libocpp internally +* **get_mo_ocsp_request_data** to include the **iso15118CertificateHashData** as part of a **DataTransfer.req(Authorize)** for Plug&Charge + if required +* **update_ocsp_cache** to update the OCSP cache which is part of a **DataTransfer.conf(GetCertificateStatus)** message from the CSMS +* **is_ca_certificate_installed** to verify if a certain CA certificate is installed +* **generate_certificate_signing_request** to generate a CSR that can be used as part of a **SignCertificate.req** and + `DataTransfer.req(SignCertificate)` message to the CSMS. +* **get_leaf_certificate_info** to get the certificate and private key path of the CSMS client certificate used for security profile 3. +* **get_verify_file** to get the path to a CA bundle that can be used for verifying, e.g., the CSMS TLS server certificate +* **get_leaf_expiry_days_count** to determine when a leaf certificate expires. This information is used by libocpp in order to renew + leaf certificates in case they expire soon + +Note that a lot of conversion between the libocpp types and the generated EVerest types are required for the given commands. Since the +conversion functionality is used by this OCPP module and the OCPP201 module, it is implemented as a +`separate library <../../lib/staging/ocpp/>`_ . + +Requires: data_transfer +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp_data_transfer <../../interfaces/ocpp_data_transfer.yaml>`_ + +This module optionally requires a connection to a module implementing the **ocpp_data_transfer** interface. This connection is used to +handle **DataTransfer.req** messages from the CSMS. A module implementing this interface can contain custom logic to handle the requests +from the CSMS. + +This module makes use of the following commands of this interface: + +* **data_transfer** to forward **DataTransfer.req** messages from the CSMS + +Requires: display_message +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `display_message <../../interfaces/display_message.yaml>`_ + +This module optionally requires a connection to a module implementing the **display_message** interface. This connection is used to allow +the CSMS to display pricing or other information on the display of a charging station. In order to fulfill the requirements of the +California Pricing whitepaper, it is required to connect a module implementing this interface. + +This module makes use of the following commands of this interface: + +* **set_display_message** to set a message on the charging station's display. This is executed when the CSMS sends a + **DataTransfer.req(SetUserPrice)** message to the charging station. + Global Errors and Error Reporting -================================= +--------------------------------- -The `enable_global_errors` flag for this module is enabled. This module is therefore able to retrieve and process all reported errors +The **enable_global_errors** flag for this module is enabled in its manifest. This module is therefore able to retrieve and process all reported errors from other modules loaded in the same EVerest configuration. -In OCPP1.6 errors and can be reported using the `StatusNotification.req` message. If this module gets notified about a raised error, -it initiates a `StatusNotification.req` that contains information about the error that has been raised. +In OCPP1.6 errors can be reported using the **StatusNotification.req** message. If this module gets notified about a raised error, +it initiates a **StatusNotification.req** that contains information about the error that has been raised. + +The field **status** of the **StatusNotification.req** will be set to faulted only in case the error is of the special type +**evse_manager/Inoperative**. The field **connectorId** is set based on the mapping (for EVSE id and connector id) of the origin of the error. +If no mapping is provided, the error will be reported on connectorId 0. Note that the mapping can be configured per module inside the +EVerest config file. + +For all other errors, raised in EVerest, the following mapping to an +OCPP **StatusNotification.req** will be used: + +* **StatusNotification.req** property ``errorCode`` will always be + ``OtherError`` +* **StatusNotification.req** property ``status`` will reflect the present status of the + charge point +* **StatusNotification.req** property ``info`` -> origin of EVerest error +* **StatusNotification.req** property ``vendorErrorCode`` -> EVerest error type and + subtype (the error type is simplified, meaning, that its leading part, + the interface name, is stripped) +* **StatusNotification.req** property ``vendorId`` -> EVerest error message + +The reason for using the **StatusNotification.req** property property +``vendorId`` for the error message is that it can carry the largest +string (255 characters), whereas the other fields (``info`` and +``vendorErrorCode``) only allow up to 50 characters. + +If for example the module with id `yeti_driver` within its +implementation with id `board_support` creates the following error: + +.. code-block:: cpp + + error_factory->create_error("evse_board_support/EnergyManagement", + "OutOfEnergy", "someone cut the wires") + +the corresponding fields in the **StatusNotification.req** message will +look like this: + +.. code-block:: JSON -The field `status` of the `StatusNotification.req` will be set to faulted only in case the error is of the special type `evse_manager/Inoperative`. -The field `connectorId` is set based on the mapping (for evse id and connector id) of the origin of the Error. -If no mapping is provided, the error will be reported on connectorId 0. Note that the mapping can be configured per module -inside the EVerest config file. -The field `errorCode` is set based in the `type` property of the error. + { + "info": "yeti_driver->board_support", + "vendorErrorCode": "EnergyManagement/OutOfEnergy", + "vendorId": "someone cut the wires" + } -The fields `info`, `vendorId` and `vendorErrorCode` are set based on the error type and the provided error properties. Please see the definiton -of `get_error_info` to see how the `StatusNotification.req` is constructed based on the given error. +The **StatusNotification.req** message has some limitations with respect +to reporting errors: -The `StatusNotification.req` message has some limitations with respect to reporting errors: -* Single errors can not simply be cleared. If multiple errors are raised it is not possible to clear individual errors -* Some fields of the message have relatively small character limits (e.g. `info` with 50 characters, `vendorErrorCode` with 50 characters) +* Single errors cannot simply be cleared. If multiple errors are raised, + it is not possible to clear individual errors. +* ``vendorId``, ``info`` and ``vendorErrorCode`` are limited in length + (see above). -This module attempts to follow the Minimum Required Error Codes (MRECS): https://inl.gov/chargex/mrec/ . This proposes a unified methodology -to define and classify a minimum required set of error codes and how to report them via OCPP1.6. +This module attempts to follow the Minimum Required Error Codes (MRECS): https://inl.gov/chargex/mrec/. This proposes a unified +methodology to define and classify a minimum required set of error codes and how to report them via OCPP1.6. This module currently deviates from the MREC specification in the following points: -* Simultaneous errors: MREC requires to report simultaneous errors by reporting them in a single `StatusNotification.req` by separating the -information of the fields `vendorId`and `info` by a semicolon. This module sends one `StatusNotifcation.req` per individual errors because -of the limited maximum characters of the `info` field. -* MREC requires to always use the value `Faulted` for the `status` field when reporting an error. The OCPP1.6 specification defines the -`Faulted` value as follows: "When a Charge Point or connector has reported an error and is not available for energy delivery . (Inoperative).". -This module therefore only reports `Faulted` when the Charge Point is not available for energy delivery. -Interaction with EVSE Manager -============================= +* Simultaneous errors: MREC requires reporting simultaneous errors by reporting them in a single **StatusNotification.req** by separating + the information of the fields **vendorId** and **info** by a semicolon. This module sends one **StatusNotification.req** per individual error + because of the limited maximum characters of the **info** field. +* MREC requires always using the value **Faulted** for the **status** field when reporting an error. The OCPP1.6 specification defines the + **Faulted** value as follows: "When a Charge Point or connector has reported an error and is not available for energy delivery. + (Inoperative)." This module, therefore, only reports **Faulted** when the Charge Point is not available for energy delivery. + +Energy Management and Smart Charging Integration +------------------------------------------------ + +OCPP1.6 defines the SmartCharging feature profile to allow the CSMS to control or influence the power consumption of the charging station. +This module integrates the composite schedule(s) within EVerest's energy management. For further information about smart charging and the +composite schedule calculation please refer to the OCPP1.6 specification. + +The integration of the composite schedules is implemented through the optional requirement(s) `evse_energy_sink` (interface: `external_energy_limits`) +of this module. Depending on the number of EVSEs configured, each composite limit is communicated via a seperate sink, including the composite schedule +for EVSE with id 0 (representing the whole charging station). The easiest way to explain this is with an example. If your charging station +has two EVSEs you need to connect three modules that implement the `external_energy_limits` interface: One representing evse id 0 and +two representing your actual EVSEs. + +📌 **Note:** You have to configure an evse mapping for each module connected via the evse_energy_sink connection. This allows the module to identify +which requirement to use when communicating the limits for the EVSEs. For more information about the module mapping please see +`3-tier module mappings `_. + +This module defines a callback that gets executed every time charging profiles are changed, added or removed by the CSMS. The callback retrieves +the composite schedules for all EVSEs (including evse id 0) and calls the `set_external_limits` command of the respective requirement that implements +the `external_energy_limits` interface. In addition, the config parameter `PublishChargingScheduleIntervalS` defines a periodic interval to retrieve +the composite schedule also in case no charging profiles have been changed. The configuration parameter `PublishChargingScheduleDurationS` defines +the duration in seconds of the requested composite schedules starting now. The value configured for `PublishChargingScheduleDurationS` shall be greater +than the value configured for `PublishChargingScheduleIntervalS` because otherwise time periods could be missed by the application. + + +Certificate Management +---------------------- + +Two leaf certificates are managed by the OCPP communication enabled by this module: + +* CSMS Leaf certificate (used for mTLS for SecurityProfile3) +* SECC Leaf certificate (Server certificate for ISO15118) + +60 seconds after the first **BootNotification.req** message has been accepted by the CSMS, the charging station will check if the existing +certificates are not present or have been expired. If this is the case, the charging station initiates the process of requesting a new +certificate by sending a certificate signing request to CSMS. + +For the CSMS Leaf certificate, this process is only triggered if SecurityProfile 3 is used. + +For the SECC Leaf certificate, this process is only triggered if Plug&Charge is enabled by setting the **ISO15118PnCEnabled** to **true**. -This module sets callbacks into libocpp to receive `ChangeAvailability.req` updates from the CSMS. +If a certificate has expired is then periodically checked every 12 hours. -These are sent to the EVSE Manager in `enable_disable` commands with a priority of 5000. ('types/energy_manager.yaml' contains the valid range.) +In addition to that, the charging station periodically updates the OCSP responses of the sub-CA certificates of the V2G certificate chain. +The OCSP response is cached and can be used as part of the ISO15118 TLS handshake with EVs. The OCSP update is by default performed +every seven days (or can be configured using the **OCSPRequestInterval** configuration key). +The timestamp of the last update is stored persistently, so that this process is not necessarily performed at every start up. -5000 is mid-range. diff --git a/modules/OCPP/manifest.yaml b/modules/OCPP/manifest.yaml index c9ae86032..46fbbac5c 100644 --- a/modules/OCPP/manifest.yaml +++ b/modules/OCPP/manifest.yaml @@ -1,19 +1,26 @@ description: A OCPP charge point / charging station module, currently targeting OCPP-J 1.6 config: ChargePointConfigPath: - description: Path to the configuration file + description: >- + Path to the ocpp configuration file. Libocpp defines a JSON schema for this file. Please refer to the documentation + of libocpp for more information about the configuration options. type: string default: ocpp-config.json UserConfigPath: - description: Path to the file of the OCPP user config + description: >- + Path to the file of the OCPP user config. The user config is used as an overlay for the original config defined + by the ChargePointConfigPath. Any changes to configuration keys turned out internally or by the CSMS will be + written to the user config file. type: string default: user_config.json DatabasePath: - description: Path to the persistent SQLite database folder + description: >- + Path to the persistent SQLite database directory. Please refer to the libocpp documentation for more information + about the database and its structure. type: string default: /tmp/ocpp_1_6_charge_point EnableExternalWebsocketControl: - description: If true websocket can be disconnected and connected externally + description: If true websocket can be disconnected and connected externally. This parameter is for debug and testing purposes. type: boolean default: false PublishChargingScheduleIntervalS: @@ -28,13 +35,24 @@ config: type: integer default: 600 MessageLogPath: - description: Path to folder where logs of all OCPP messages get written to + description: Path to directory where logs of all OCPP messages are written to type: string default: /tmp/everest_ocpp_logs MessageQueueResumeDelay: - description: Time (seconds) to delay resuming the message queue after reconnecting + description: >- + Time (seconds) to delay resuming the message queue after reconnecting. This parameter was introduced because + some OCTT test cases require that the first message after a reconnect is sent by the CSMS. type: integer default: 0 + RequestCompositeScheduleUnit: + description: >- + Unit in which composite schedules are requested and shared within EVerest. It is recommended to use + Amps for AC and Watts for DC charging stations. + Allowed values: + - 'A' for Amps + - 'W' for Watts + type: string + default: 'A' provides: main: description: This is a OCPP 1.6 charge point @@ -59,10 +77,10 @@ requires: interface: evse_manager min_connections: 1 max_connections: 128 - connector_zero_sink: + evse_energy_sink: interface: external_energy_limits min_connections: 0 - max_connections: 1 + max_connections: 129 reservation: interface: reservation min_connections: 1 diff --git a/modules/OCPP/ocpp_generic/ocppImpl.cpp b/modules/OCPP/ocpp_generic/ocppImpl.cpp index 55a5dbf80..b457c3a80 100644 --- a/modules/OCPP/ocpp_generic/ocppImpl.cpp +++ b/modules/OCPP/ocpp_generic/ocppImpl.cpp @@ -4,6 +4,8 @@ #include "ocppImpl.hpp" #include "ocpp/v16/messages/ChangeAvailability.hpp" +#include + namespace module { namespace ocpp_generic { @@ -124,11 +126,7 @@ bool ocppImpl::handle_restart() { void ocppImpl::handle_security_event(types::ocpp::SecurityEvent& event) { std::optional timestamp; if (event.timestamp.has_value()) { - try { - timestamp = ocpp::DateTime(event.timestamp.value()); - } catch (...) { - EVLOG_warning << "Timestamp in security event could not be parsed, using current datetime."; - } + timestamp = ocpp_conversions::to_ocpp_datetime_or_now(event.timestamp.value()); } this->mod->charge_point->on_security_event(event.type, event.info, event.critical, timestamp); } 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/CMakeLists.txt b/modules/OCPP201/CMakeLists.txt index 4a6d2e38d..bf6db434d 100644 --- a/modules/OCPP201/CMakeLists.txt +++ b/modules/OCPP201/CMakeLists.txt @@ -18,17 +18,25 @@ target_link_libraries(${MODULE_NAME} everest::ocpp everest::ocpp_evse_security everest::ocpp_conversions + everest::external_energy_limits +) + +target_compile_options(${MODULE_NAME} + PRIVATE + -Wimplicit-fallthrough + -Werror=switch-enum ) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} PRIVATE - "main/emptyImpl.cpp" "auth_validator/auth_token_validatorImpl.cpp" "auth_provider/auth_token_providerImpl.cpp" "data_transfer/ocpp_data_transferImpl.cpp" "ocpp_generic/ocppImpl.cpp" "session_cost/session_costImpl.cpp" + "device_model/everest_device_model_storage.cpp" + "device_model/composed_device_model_storage.cpp" ) # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index 101528839..0f63b9f65 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -8,7 +8,10 @@ #include #include +#include +#include #include +#include #include namespace module { @@ -30,9 +33,22 @@ TxEvent get_tx_event(const ocpp::v201::ReasonEnum reason) { return TxEvent::EV_DISCONNECTED; case ocpp::v201::ReasonEnum::ImmediateReset: return TxEvent::IMMEDIATE_RESET; - default: + // FIXME(kai): these reasons definitely do not all map to NONE + case ocpp::v201::ReasonEnum::EmergencyStop: + case ocpp::v201::ReasonEnum::EnergyLimitReached: + case ocpp::v201::ReasonEnum::GroundFault: + case ocpp::v201::ReasonEnum::LocalOutOfCredit: + case ocpp::v201::ReasonEnum::Other: + case ocpp::v201::ReasonEnum::OvercurrentFault: + case ocpp::v201::ReasonEnum::PowerLoss: + case ocpp::v201::ReasonEnum::PowerQuality: + case ocpp::v201::ReasonEnum::Reboot: + case ocpp::v201::ReasonEnum::SOCLimitReached: + case ocpp::v201::ReasonEnum::TimeLimitReached: + case ocpp::v201::ReasonEnum::Timeout: return TxEvent::NONE; } + return TxEvent::NONE; } std::set get_tx_start_stop_points(const std::string& tx_start_stop_point_csl) { @@ -258,10 +274,19 @@ bool OCPP201::all_evse_ready() { } void OCPP201::init() { - invoke_init(*p_main); invoke_init(*p_auth_provider); invoke_init(*p_auth_validator); + // ensure all evse_energy_sink(s) that are connected have an evse id mapping + for (const auto& evse_sink : this->r_evse_energy_sink) { + if (not evse_sink->get_mapping().has_value()) { + EVLOG_critical << "Please configure an evse mapping your configuration file for the connected " + "r_evse_energy_sink with module_id: " + << evse_sink->module_id; + throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse."); + } + } + this->init_evse_maps(); for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) { @@ -277,7 +302,6 @@ void OCPP201::init() { } void OCPP201::ready() { - invoke_ready(*p_main); invoke_ready(*p_auth_provider); invoke_ready(*p_auth_validator); @@ -382,14 +406,20 @@ void OCPP201::ready() { provided_token.connectors = std::vector{request.evseId.value()}; } this->p_auth_provider->publish_provided_token(provided_token); + return ocpp::v201::RequestStartStopStatusEnum::Accepted; }; callbacks.stop_transaction_callback = [this](const int32_t evse_id, const ocpp::v201::ReasonEnum& stop_reason) { - if (evse_id > 0 && evse_id <= this->r_evse_manager.size()) { - types::evse_manager::StopTransactionRequest req; - req.reason = conversions::to_everest_stop_transaction_reason(stop_reason); - this->r_evse_manager.at(evse_id - 1)->call_stop_transaction(req); + if (evse_id <= 0 or evse_id > this->r_evse_manager.size()) { + return ocpp::v201::RequestStartStopStatusEnum::Rejected; } + + types::evse_manager::StopTransactionRequest req; + req.reason = conversions::to_everest_stop_transaction_reason(stop_reason); + + return this->r_evse_manager.at(evse_id - 1)->call_stop_transaction(req) + ? ocpp::v201::RequestStartStopStatusEnum::Accepted + : ocpp::v201::RequestStartStopStatusEnum::Rejected; }; callbacks.pause_charging_callback = [this](const int32_t evse_id) { @@ -419,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) { @@ -478,7 +520,15 @@ void OCPP201::ready() { }; callbacks.configure_network_connection_profile_callback = - [this](const ocpp::v201::NetworkConnectionProfile& network_connection_profile) { return true; }; + [this](const int32_t configuration_slot, + const ocpp::v201::NetworkConnectionProfile& network_connection_profile) { + std::promise promise; + std::future future = promise.get_future(); + ocpp::v201::ConfigNetworkResult result; + result.success = true; + promise.set_value(result); + return future; + }; callbacks.all_connectors_unavailable_callback = [this]() { EVLOG_info << "All connectors unavailable, proceed with firmware installation"; @@ -596,9 +646,11 @@ void OCPP201::ready() { }; } - callbacks.connection_state_changed_callback = [this](const bool is_connected) { - this->p_ocpp_generic->publish_is_connected(is_connected); - }; + callbacks.connection_state_changed_callback = + [this](const bool is_connected, const int /*configuration_slot*/, + const ocpp::v201::NetworkConnectionProfile& /*network_connection_profile*/) { + this->p_ocpp_generic->publish_is_connected(is_connected); + }; callbacks.security_event_callback = [this](const ocpp::CiString<50>& event_type, const std::optional>& tech_info) { @@ -613,8 +665,8 @@ void OCPP201::ready() { const auto composite_schedule_unit = get_unit_or_default(this->config.RequestCompositeScheduleUnit); - // this callback publishes the schedules within EVerest and applies the schedules for the individual evse_manager(s) - // and the connector_zero_sink + // this callback publishes the schedules within EVerest and applies the schedules for the individual + // r_evse_energy_sink const auto charging_schedules_callback = [this, composite_schedule_unit]() { const auto composite_schedules = this->charge_point->get_all_composite_schedules( this->config.RequestCompositeScheduleDurationS, composite_schedule_unit); @@ -628,13 +680,70 @@ 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(); + std::unique_ptr device_model_storage = + std::make_unique( + device_model_database_path, true, device_model_database_migration_path, device_model_config_path); this->charge_point = std::make_unique( - evse_connector_structure, device_model_database_path, true, device_model_database_migration_path, - device_model_config_path, this->ocpp_share_path.string(), this->config.CoreDatabasePath, sql_init_path.string(), - this->config.MessageLogPath, std::make_shared(*this->r_security), callbacks); + evse_connector_structure, std::move(device_model_storage), this->ocpp_share_path.string(), + 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(); @@ -735,10 +844,13 @@ void OCPP201::ready() { conversions::to_ocpp_get_15118_certificate_request(certificate_request)); EVLOG_debug << "Received response from get_15118_ev_certificate_request: " << ocpp_response; // transform response, inject action, send to associated EvseManager - const auto everest_response_status = - conversions::to_everest_iso15118_charger_status(ocpp_response.status); - const types::iso15118_charger::ResponseExiStreamStatus everest_response{ - everest_response_status, certificate_request.certificate_action, ocpp_response.exiResponse}; + types::iso15118_charger::ResponseExiStreamStatus everest_response; + everest_response.status = conversions::to_everest_iso15118_charger_status(ocpp_response.status); + everest_response.certificate_action = certificate_request.certificate_action; + if (not ocpp_response.exiResponse.get().empty()) { + // since exi_response is an optional in the EVerest type we only set it when not empty + everest_response.exi_response = ocpp_response.exiResponse.get(); + } this->r_evse_manager.at(evse_id - 1)->call_set_get_certificate_response(everest_response); }); @@ -751,7 +863,7 @@ void OCPP201::ready() { }; // A permanent fault from the evse requirement indicates that the evse should move to faulted state - evse->subscribe_error("evse_manager/Inoperative", fault_handler, fault_cleared_handler); + evse->subscribe_error(EVSE_MANAGER_INOPERATIVE_ERROR, fault_handler, fault_cleared_handler); evse_id++; } @@ -765,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); @@ -829,8 +961,27 @@ void OCPP201::process_session_event(const int32_t evse_id, const types::evse_man this->process_deauthorized(evse_id, connector_id, session_event); break; } - - // missing AuthRequired, PrepareCharging and many more + 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: + case types::evse_manager::SessionEventEnum::PrepareCharging: + case types::evse_manager::SessionEventEnum::WaitingForEnergy: + case types::evse_manager::SessionEventEnum::StoppingCharging: + case types::evse_manager::SessionEventEnum::ChargingFinished: + case types::evse_manager::SessionEventEnum::ReplugStarted: + case types::evse_manager::SessionEventEnum::ReplugFinished: + case types::evse_manager::SessionEventEnum::PluginTimeout: + case types::evse_manager::SessionEventEnum::SwitchingPhases: + case types::evse_manager::SessionEventEnum::SessionResumed: + break; } // process authorized event which will inititate a TransactionEvent(Updated) message in case the token has not yet @@ -851,7 +1002,7 @@ void OCPP201::process_tx_event_effect(const int32_t evse_id, const TxEventEffect if (transaction_data == nullptr) { throw std::runtime_error("Could not start transaction because no tranasaction data is present"); } - transaction_data->timestamp = ocpp::DateTime(session_event.timestamp); + transaction_data->timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp); if (tx_event_effect == TxEventEffect::START_TRANSACTION) { transaction_data->started = true; @@ -906,7 +1057,7 @@ void OCPP201::process_session_started(const int32_t evse_id, const int32_t conne trigger_reason = ocpp::v201::TriggerReasonEnum::RemoteStart; } } - const auto timestamp = ocpp::DateTime(session_event.timestamp); + const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp); const auto reservation_id = session_started.reservation_id; // this is always the first transaction related interaction, so we create TransactionData here @@ -963,7 +1114,9 @@ void OCPP201::process_transaction_started(const int32_t evse_id, const int32_t c auto tx_event = TxEvent::AUTHORIZED; auto trigger_reason = ocpp::v201::TriggerReasonEnum::Authorized; const auto transaction_started = session_event.transaction_started.value(); - transaction_data->reservation_id = transaction_started.reservation_id; + if (transaction_started.reservation_id.has_value()) { + transaction_data->reservation_id = transaction_started.reservation_id; + } transaction_data->remote_start_id = transaction_started.id_tag.request_id; const auto id_token = conversions::to_ocpp_id_token(transaction_started.id_tag.id_token); transaction_data->id_token = id_token; @@ -1127,6 +1280,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); @@ -1139,6 +1300,12 @@ void OCPP201::set_external_limits(const std::vectorr_evse_energy_sink, evse_id)) { + EVLOG_warning << "Can not apply external limits! No evse energy sink configured for evse_id: " << evse_id; + continue; + } + types::energy::ExternalLimits limits; std::vector schedule_import; @@ -1160,19 +1327,8 @@ void OCPP201::set_external_limits(const std::vectorr_connector_zero_sink.empty()) { - EVLOG_debug << "OCPP sets the following external limits for evse 0: \n" << limits; - this->r_connector_zero_sink.at(0)->call_set_external_limits(limits); - } else { - EVLOG_debug << "OCPP cannot set external limits for evse 0. No " - "sink is configured."; - } - } else { - EVLOG_debug << "OCPP sets the following external limits for evse " << composite_schedule.evseId << ": \n" - << limits; - this->r_evse_manager.at(composite_schedule.evseId - 1)->call_set_external_limits(limits); - } + auto& evse_sink = external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, evse_id); + evse_sink.call_set_external_limits(limits); } } diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index 253a06715..42ef16113 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -13,7 +13,6 @@ // headers for provided interface implementations #include #include -#include #include #include #include @@ -25,6 +24,7 @@ #include #include #include +#include #include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 @@ -53,7 +53,7 @@ struct Conf { class OCPP201 : public Everest::ModuleBase { public: OCPP201() = delete; - OCPP201(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr p_main, + OCPP201(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr p_auth_validator, std::unique_ptr p_auth_provider, std::unique_ptr p_data_transfer, std::unique_ptr p_ocpp_generic, @@ -61,11 +61,11 @@ class OCPP201 : public Everest::ModuleBase { std::vector> r_evse_manager, std::unique_ptr r_system, std::unique_ptr r_security, std::vector> r_data_transfer, std::unique_ptr r_auth, - std::vector> r_connector_zero_sink, - std::vector> r_display_message, Conf& config) : + std::vector> r_evse_energy_sink, + std::vector> r_display_message, + std::vector> r_reservation, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), - p_main(std::move(p_main)), p_auth_validator(std::move(p_auth_validator)), p_auth_provider(std::move(p_auth_provider)), p_data_transfer(std::move(p_data_transfer)), @@ -76,13 +76,13 @@ class OCPP201 : public Everest::ModuleBase { r_security(std::move(r_security)), r_data_transfer(std::move(r_data_transfer)), r_auth(std::move(r_auth)), - r_connector_zero_sink(std::move(r_connector_zero_sink)), + 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) { } Everest::MqttProvider& mqtt; - const std::unique_ptr p_main; const std::unique_ptr p_auth_validator; const std::unique_ptr p_auth_provider; const std::unique_ptr p_data_transfer; @@ -93,8 +93,9 @@ class OCPP201 : public Everest::ModuleBase { const std::unique_ptr r_security; const std::vector> r_data_transfer; const std::unique_ptr r_auth; - const std::vector> r_connector_zero_sink; + 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 @@ -122,6 +123,7 @@ class OCPP201 : public Everest::ModuleBase { // key represents evse_id, value indicates if ready std::map evse_ready_map; std::map> evse_soc_map; + int32_t event_id_counter{0}; std::mutex evse_ready_mutex; std::mutex session_event_mutex; std::condition_variable evse_ready_cv; @@ -155,11 +157,13 @@ 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 evse_manager and the connector_zero_sink + /// \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 7cf9467e4..ecfaf1ae8 100644 --- a/modules/OCPP201/conversions.cpp +++ b/modules/OCPP201/conversions.cpp @@ -3,6 +3,7 @@ #include #include +#include namespace module { namespace conversions { @@ -36,9 +37,8 @@ ocpp::v201::FirmwareStatusEnum to_ocpp_firmware_status_enum(const types::system: return ocpp::v201::FirmwareStatusEnum::InvalidSignature; case types::system::FirmwareUpdateStatusEnum::SignatureVerified: return ocpp::v201::FirmwareStatusEnum::SignatureVerified; - default: - throw std::out_of_range("Could not convert FirmwareUpdateStatusEnum to FirmwareStatusEnum"); } + throw std::out_of_range("Could not convert FirmwareUpdateStatusEnum to FirmwareStatusEnum"); } ocpp::v201::DataTransferStatusEnum to_ocpp_data_transfer_status_enum(types::ocpp::DataTransferStatus status) { @@ -51,9 +51,10 @@ ocpp::v201::DataTransferStatusEnum to_ocpp_data_transfer_status_enum(types::ocpp return ocpp::v201::DataTransferStatusEnum::UnknownMessageId; case types::ocpp::DataTransferStatus::UnknownVendorId: return ocpp::v201::DataTransferStatusEnum::UnknownVendorId; - default: + case types::ocpp::DataTransferStatus::Offline: return ocpp::v201::DataTransferStatusEnum::UnknownVendorId; } + return ocpp::v201::DataTransferStatusEnum::UnknownVendorId; } ocpp::v201::DataTransferRequest to_ocpp_data_transfer_request(types::ocpp::DataTransferRequest request) { @@ -137,29 +138,36 @@ to_ocpp_meter_value(const types::powermeter::Powermeter& power_meter, const ocpp::v201::ReadingContextEnum& reading_context, const std::optional signed_meter_value) { ocpp::v201::MeterValue meter_value; - meter_value.timestamp = ocpp::DateTime(power_meter.timestamp); + meter_value.timestamp = ocpp_conversions::to_ocpp_datetime_or_now(power_meter.timestamp); + + bool energy_Wh_import_signed_total_added = false; + // individual signed meter values can be provided by the power_meter itself - // signed_meter_value is intended for OCMF style blobs of signed meter value reports during transaction start or end - // This is interpreted as Energy.Active.Import.Register ocpp::v201::SampledValue sampled_value = to_ocpp_sampled_value( reading_context, ocpp::v201::MeasurandEnum::Energy_Active_Import_Register, "Wh", std::nullopt); - sampled_value.value = power_meter.energy_Wh_import.total; - // add signedMeterValue if present - if (signed_meter_value.has_value()) { - sampled_value.signedMeterValue = to_ocpp_signed_meter_value(signed_meter_value.value()); - } - meter_value.sampledValue.push_back(sampled_value); - - // individual signed meter values can be provided by the power_meter itself // Energy.Active.Import.Register if (power_meter.energy_Wh_import_signed.has_value()) { - sampled_value = to_ocpp_sampled_value(reading_context, ocpp::v201::MeasurandEnum::Energy_Active_Import_Register, - "Wh", std::nullopt); sampled_value.value = power_meter.energy_Wh_import.total; const auto& energy_Wh_import_signed = power_meter.energy_Wh_import_signed.value(); if (energy_Wh_import_signed.total.has_value()) { sampled_value.signedMeterValue = to_ocpp_signed_meter_value(energy_Wh_import_signed.total.value()); + energy_Wh_import_signed_total_added = true; + } + meter_value.sampledValue.push_back(sampled_value); + } + + if (not energy_Wh_import_signed_total_added) { + // No signed meter value for Energy.Active.Import.Register added, either no signed meter values are available or + // just one global signed_meter_value is present signed_meter_value is intended for OCMF style blobs of signed + // meter value reports during transaction start or end + // This is interpreted as Energy.Active.Import.Register + sampled_value = to_ocpp_sampled_value(reading_context, ocpp::v201::MeasurandEnum::Energy_Active_Import_Register, + "Wh", std::nullopt); + sampled_value.value = power_meter.energy_Wh_import.total; + // add signedMeterValue if present + if (signed_meter_value.has_value()) { + sampled_value.signedMeterValue = to_ocpp_signed_meter_value(signed_meter_value.value()); } meter_value.sampledValue.push_back(sampled_value); } @@ -482,9 +490,8 @@ ocpp::v201::LogStatusEnum to_ocpp_log_status_enum(types::system::UploadLogsStatu return ocpp::v201::LogStatusEnum::Rejected; case types::system::UploadLogsStatus::AcceptedCanceled: return ocpp::v201::LogStatusEnum::AcceptedCanceled; - default: - throw std::runtime_error("Could not convert UploadLogsStatus"); } + throw std::runtime_error("Could not convert UploadLogsStatus"); } ocpp::v201::GetLogResponse to_ocpp_get_log_response(const types::system::UploadLogsResponse& response) { @@ -507,9 +514,8 @@ to_ocpp_update_firmware_status_enum(const types::system::UpdateFirmwareResponse& return ocpp::v201::UpdateFirmwareStatusEnum::InvalidCertificate; case types::system::UpdateFirmwareResponse::RevokedCertificate: return ocpp::v201::UpdateFirmwareStatusEnum::RevokedCertificate; - default: - throw std::runtime_error("Could not convert UpdateFirmwareResponse"); } + throw std::runtime_error("Could not convert UpdateFirmwareResponse"); } ocpp::v201::UpdateFirmwareResponse @@ -537,9 +543,8 @@ ocpp::v201::UploadLogStatusEnum to_ocpp_upload_logs_status_enum(types::system::L return ocpp::v201::UploadLogStatusEnum::Uploading; case types::system::LogStatusEnum::AcceptedCanceled: return ocpp::v201::UploadLogStatusEnum::AcceptedCanceled; - default: - throw std::runtime_error("Could not convert UploadLogStatusEnum"); } + throw std::runtime_error("Could not convert UploadLogStatusEnum"); } ocpp::v201::BootReasonEnum to_ocpp_boot_reason(types::system::BootReason reason) { @@ -562,9 +567,8 @@ ocpp::v201::BootReasonEnum to_ocpp_boot_reason(types::system::BootReason reason) return ocpp::v201::BootReasonEnum::Unknown; case types::system::BootReason::Watchdog: return ocpp::v201::BootReasonEnum::Watchdog; - default: - throw std::runtime_error("Could not convert BootReasonEnum"); } + throw std::runtime_error("Could not convert BootReasonEnum"); } ocpp::v201::ReasonEnum to_ocpp_reason(types::evse_manager::StopTransactionReason reason) { @@ -607,9 +611,11 @@ ocpp::v201::ReasonEnum to_ocpp_reason(types::evse_manager::StopTransactionReason return ocpp::v201::ReasonEnum::TimeLimitReached; case types::evse_manager::StopTransactionReason::Timeout: return ocpp::v201::ReasonEnum::Timeout; - default: + case types::evse_manager::StopTransactionReason::SoftReset: + case types::evse_manager::StopTransactionReason::UnlockCommand: return ocpp::v201::ReasonEnum::Other; } + return ocpp::v201::ReasonEnum::Other; } ocpp::v201::IdTokenEnum to_ocpp_id_token_enum(types::authorization::IdTokenType id_token_type) { @@ -630,9 +636,8 @@ ocpp::v201::IdTokenEnum to_ocpp_id_token_enum(types::authorization::IdTokenType return ocpp::v201::IdTokenEnum::Local; case types::authorization::IdTokenType::NoAuthorization: return ocpp::v201::IdTokenEnum::NoAuthorization; - default: - throw std::runtime_error("Could not convert IdTokenEnum"); } + throw std::runtime_error("Could not convert IdTokenEnum"); } ocpp::v201::IdToken to_ocpp_id_token(const types::authorization::IdToken& id_token) { @@ -700,9 +705,8 @@ to_everest_stop_transaction_reason(const ocpp::v201::ReasonEnum& stop_reason) { return types::evse_manager::StopTransactionReason::TimeLimitReached; case ocpp::v201::ReasonEnum::Timeout: return types::evse_manager::StopTransactionReason::Timeout; - default: - return types::evse_manager::StopTransactionReason::Other; } + return types::evse_manager::StopTransactionReason::Other; } std::vector to_ocpp_ocsp_request_data_vector( @@ -729,10 +733,9 @@ ocpp::v201::HashAlgorithmEnum to_ocpp_hash_algorithm_enum(const types::iso15118_ return ocpp::v201::HashAlgorithmEnum::SHA384; case types::iso15118_charger::HashAlgorithm::SHA512: return ocpp::v201::HashAlgorithmEnum::SHA512; - default: - throw std::out_of_range( - "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v201::HashAlgorithmEnum"); } + throw std::out_of_range( + "Could not convert types::iso15118_charger::HashAlgorithm to ocpp::v201::HashAlgorithmEnum"); } std::vector @@ -811,9 +814,8 @@ ocpp::v201::AttributeEnum to_ocpp_attribute_enum(const types::ocpp::AttributeEnu return ocpp::v201::AttributeEnum::MinSet; case types::ocpp::AttributeEnum::MaxSet: return ocpp::v201::AttributeEnum::MaxSet; - default: - throw std::out_of_range("Could not convert AttributeEnum"); } + throw std::out_of_range("Could not convert AttributeEnum"); } ocpp::v201::Get15118EVCertificateRequest @@ -825,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(); @@ -883,9 +920,8 @@ types::ocpp::DataTransferStatus to_everest_data_transfer_status(ocpp::v201::Data return types::ocpp::DataTransferStatus::UnknownMessageId; case ocpp::v201::DataTransferStatusEnum::UnknownVendorId: return types::ocpp::DataTransferStatus::UnknownVendorId; - default: - return types::ocpp::DataTransferStatus::UnknownVendorId; } + return types::ocpp::DataTransferStatus::UnknownVendorId; } types::ocpp::DataTransferRequest to_everest_data_transfer_request(ocpp::v201::DataTransferRequest request) { @@ -986,10 +1022,9 @@ to_everest_authorization_status(const ocpp::v201::AuthorizationStatusEnum status return types::authorization::AuthorizationStatus::NotAtThisTime; case ocpp::v201::AuthorizationStatusEnum::Unknown: return types::authorization::AuthorizationStatus::Unknown; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::AuthorizationStatusEnum to types::authorization::AuthorizationStatus"); } + throw std::out_of_range( + "Could not convert ocpp::v201::AuthorizationStatusEnum to types::authorization::AuthorizationStatus"); } types::authorization::IdTokenType to_everest_id_token_type(const ocpp::v201::IdTokenEnum& type) { @@ -1010,9 +1045,8 @@ types::authorization::IdTokenType to_everest_id_token_type(const ocpp::v201::IdT return types::authorization::IdTokenType::MacAddress; case ocpp::v201::IdTokenEnum::NoAuthorization: return types::authorization::IdTokenType::NoAuthorization; - default: - throw std::out_of_range("Could not convert ocpp::v201::IdTokenEnum to types::authorization::IdTokenType"); } + throw std::out_of_range("Could not convert ocpp::v201::IdTokenEnum to types::authorization::IdTokenType"); } types::authorization::IdToken to_everest_id_token(const ocpp::v201::IdToken& id_token) { @@ -1039,10 +1073,9 @@ to_everest_certificate_status(const ocpp::v201::AuthorizeCertificateStatusEnum s return types::authorization::CertificateStatus::CertChainError; case ocpp::v201::AuthorizeCertificateStatusEnum::ContractCancelled: return types::authorization::CertificateStatus::ContractCancelled; - default: - throw std::out_of_range("Could not convert ocpp::v201::AuthorizeCertificateStatusEnum to " - "types::authorization::CertificateStatus"); } + throw std::out_of_range("Could not convert ocpp::v201::AuthorizeCertificateStatusEnum to " + "types::authorization::CertificateStatus"); } types::ocpp::OcppTransactionEvent @@ -1060,21 +1093,9 @@ to_everest_ocpp_transaction_event(const ocpp::v201::TransactionEventRequest& tra break; } - auto evse_id = 1; - auto connector_id = 1; - if (transaction_event.evse.has_value()) { - evse_id = transaction_event.evse.value().id; - if (transaction_event.evse.value().connectorId.has_value()) { - connector_id = transaction_event.evse.value().connectorId.value(); - } - } else { - EVLOG_warning << "Attempting to convert TransactionEventRequest that does not contain information about the " - "EVSE. evse_id and connector default to 1."; + ocpp_transaction_event.evse = to_everest_evse(transaction_event.evse.value()); } - - ocpp_transaction_event.evse_id = evse_id; - ocpp_transaction_event.connector = connector_id; ocpp_transaction_event.session_id = transaction_event.transactionInfo.transactionId; // session_id == transaction_id for OCPP2.0.1 ocpp_transaction_event.transaction_id = transaction_event.transactionInfo.transactionId; @@ -1091,9 +1112,8 @@ types::display_message::MessageFormat to_everest_message_format(const ocpp::v201 return types::display_message::MessageFormat::URI; case ocpp::v201::MessageFormatEnum::UTF8: return types::display_message::MessageFormat::UTF8; - default: - throw std::out_of_range("Could not convert ocpp::v201::MessageFormatEnum to types::ocpp::MessageFormat"); } + throw std::out_of_range("Could not convert ocpp::v201::MessageFormatEnum to types::ocpp::MessageFormat"); } types::display_message::MessageContent to_everest_message_content(const ocpp::v201::MessageContent& message_content) { @@ -1140,10 +1160,8 @@ to_everest_registration_status(const ocpp::v201::RegistrationStatusEnum& registr return types::ocpp::RegistrationStatus::Pending; case ocpp::v201::RegistrationStatusEnum::Rejected: return types::ocpp::RegistrationStatus::Rejected; - default: - throw std::out_of_range( - "Could not convert ocpp::v201::RegistrationStatusEnum to types::ocpp::RegistrationStatus"); } + throw std::out_of_range("Could not convert ocpp::v201::RegistrationStatusEnum to types::ocpp::RegistrationStatus"); } types::ocpp::StatusInfoType to_everest_status_info_type(const ocpp::v201::StatusInfo& status_info) { @@ -1231,9 +1249,8 @@ types::ocpp::AttributeEnum to_everest_attribute_enum(const ocpp::v201::Attribute return types::ocpp::AttributeEnum::MinSet; case ocpp::v201::AttributeEnum::MaxSet: return types::ocpp::AttributeEnum::MaxSet; - default: - throw std::out_of_range("Could not convert AttributeEnum"); } + throw std::out_of_range("Could not convert AttributeEnum"); } types::ocpp::GetVariableStatusEnumType @@ -1249,9 +1266,8 @@ to_everest_get_variable_status_enum_type(const ocpp::v201::GetVariableStatusEnum return types::ocpp::GetVariableStatusEnumType::UnknownVariable; case ocpp::v201::GetVariableStatusEnum::NotSupportedAttributeType: return types::ocpp::GetVariableStatusEnumType::NotSupportedAttributeType; - default: - throw std::out_of_range("Could not convert GetVariableStatusEnumType"); } + throw std::out_of_range("Could not convert GetVariableStatusEnumType"); } types::ocpp::SetVariableStatusEnumType @@ -1269,9 +1285,8 @@ to_everest_set_variable_status_enum_type(const ocpp::v201::SetVariableStatusEnum return types::ocpp::SetVariableStatusEnumType::NotSupportedAttributeType; case ocpp::v201::SetVariableStatusEnum::RebootRequired: return types::ocpp::SetVariableStatusEnumType::RebootRequired; - default: - throw std::out_of_range("Could not convert GetVariableStatusEnumType"); } + throw std::out_of_range("Could not convert GetVariableStatusEnumType"); } types::ocpp::ChargingSchedules @@ -1416,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/device_model/composed_device_model_storage.cpp b/modules/OCPP201/device_model/composed_device_model_storage.cpp new file mode 100644 index 000000000..bb7f7c497 --- /dev/null +++ b/modules/OCPP201/device_model/composed_device_model_storage.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include + +static constexpr auto VARIABLE_SOURCE_OCPP = "OCPP"; + +namespace module::device_model { +ComposedDeviceModelStorage::ComposedDeviceModelStorage(const std::string& libocpp_device_model_storage_address, + const bool libocpp_initialize_device_model, + const std::string& device_model_migration_path, + const std::string& device_model_config_path) : + everest_device_model_storage(std::make_unique()), + libocpp_device_model_storage(std::make_unique( + libocpp_device_model_storage_address, device_model_migration_path, device_model_config_path, + libocpp_initialize_device_model)), + device_model_map(get_device_model()) { +} + +ocpp::v201::DeviceModelMap ComposedDeviceModelStorage::get_device_model() { + ocpp::v201::DeviceModelMap everest_dm = everest_device_model_storage->get_device_model(); + ocpp::v201::DeviceModelMap libocpp_dm = libocpp_device_model_storage->get_device_model(); + everest_dm.merge(libocpp_dm); + return everest_dm; +} + +std::optional +ComposedDeviceModelStorage::get_variable_attribute(const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum) { + if (get_variable_source(component_id, variable_id) == VARIABLE_SOURCE_OCPP) { + return libocpp_device_model_storage->get_variable_attribute(component_id, variable_id, attribute_enum); + } + + return everest_device_model_storage->get_variable_attribute(component_id, variable_id, attribute_enum); +} + +std::vector +ComposedDeviceModelStorage::get_variable_attributes(const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id, + const std::optional& attribute_enum) { + if (get_variable_source(component_id, variable_id) == VARIABLE_SOURCE_OCPP) { + return libocpp_device_model_storage->get_variable_attributes(component_id, variable_id, attribute_enum); + } + + return everest_device_model_storage->get_variable_attributes(component_id, variable_id, attribute_enum); +} + +bool ComposedDeviceModelStorage::set_variable_attribute_value(const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum, + const std::string& value, const std::string& source) { + if (get_variable_source(component_id, variable_id) == VARIABLE_SOURCE_OCPP) { + return libocpp_device_model_storage->set_variable_attribute_value(component_id, variable_id, attribute_enum, + value, source); + } + return everest_device_model_storage->set_variable_attribute_value(component_id, variable_id, attribute_enum, value, + source); +} + +std::optional +ComposedDeviceModelStorage::set_monitoring_data(const ocpp::v201::SetMonitoringData& data, + const ocpp::v201::VariableMonitorType type) { + return libocpp_device_model_storage->set_monitoring_data(data, type); +} + +bool ComposedDeviceModelStorage::update_monitoring_reference(const int32_t monitor_id, + const std::string& reference_value) { + return libocpp_device_model_storage->update_monitoring_reference(monitor_id, reference_value); +} + +std::vector +ComposedDeviceModelStorage::get_monitoring_data(const std::vector& criteria, + const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id) { + if (get_variable_source(component_id, variable_id) == VARIABLE_SOURCE_OCPP) { + return libocpp_device_model_storage->get_monitoring_data(criteria, component_id, variable_id); + } + + return everest_device_model_storage->get_monitoring_data(criteria, component_id, variable_id); +} + +ocpp::v201::ClearMonitoringStatusEnum ComposedDeviceModelStorage::clear_variable_monitor(int monitor_id, + bool allow_protected) { + return libocpp_device_model_storage->clear_variable_monitor(monitor_id, allow_protected); +} + +int32_t ComposedDeviceModelStorage::clear_custom_variable_monitors() { + return libocpp_device_model_storage->clear_custom_variable_monitors(); +} + +void ComposedDeviceModelStorage::check_integrity() { + everest_device_model_storage->check_integrity(); + libocpp_device_model_storage->check_integrity(); +} + +std::string +module::device_model::ComposedDeviceModelStorage::get_variable_source(const ocpp::v201::Component& component, + const ocpp::v201::Variable& variable) { + std::optional variable_source = device_model_map[component][variable].source; + if (variable_source.has_value() && variable_source.value() != VARIABLE_SOURCE_OCPP) { + // For now, this just throws because we only have the libocpp source. When the config service is + // implemented, this should not throw. + throw ocpp::v201::DeviceModelError("Source is not 'OCPP', not sure what to do"); + } + + return VARIABLE_SOURCE_OCPP; +} + +} // namespace module::device_model diff --git a/modules/OCPP201/device_model/composed_device_model_storage.hpp b/modules/OCPP201/device_model/composed_device_model_storage.hpp new file mode 100644 index 000000000..073db23eb --- /dev/null +++ b/modules/OCPP201/device_model/composed_device_model_storage.hpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include + +namespace module::device_model { +class ComposedDeviceModelStorage : public ocpp::v201::DeviceModelStorageInterface { +private: // Members + std::unique_ptr everest_device_model_storage; + std::unique_ptr libocpp_device_model_storage; + ocpp::v201::DeviceModelMap device_model_map; + +public: + ComposedDeviceModelStorage(const std::string& libocpp_device_model_storage_address, + const bool libocpp_initialize_device_model, + const std::string& device_model_migration_path, + const std::string& device_model_config_path); + virtual ~ComposedDeviceModelStorage() override = default; + virtual ocpp::v201::DeviceModelMap get_device_model() override; + virtual std::optional + get_variable_attribute(const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum) override; + virtual std::vector + get_variable_attributes(const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id, + const std::optional& attribute_enum) override; + virtual bool set_variable_attribute_value(const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum, const std::string& value, + const std::string& source) override; + virtual std::optional + set_monitoring_data(const ocpp::v201::SetMonitoringData& data, const ocpp::v201::VariableMonitorType type) override; + virtual bool update_monitoring_reference(const int32_t monitor_id, const std::string& reference_value) override; + virtual std::vector + get_monitoring_data(const std::vector& criteria, + const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id) override; + virtual ocpp::v201::ClearMonitoringStatusEnum clear_variable_monitor(int monitor_id, bool allow_protected) override; + virtual int32_t clear_custom_variable_monitors() override; + virtual void check_integrity() override; + +private: // Functions + /// + /// \brief Get variable source of given variable. + /// \param component Component the variable belongs to. + /// \param variable The variable to get the source from. + /// \return The variable source. Defaults to 'OCPP'. + /// \throws DeviceModelError When source is something else than 'OCPP' (not implemented yet) + /// + std::string get_variable_source(const ocpp::v201::Component& component, const ocpp::v201::Variable& variable); +}; +} // namespace module::device_model diff --git a/modules/OCPP201/device_model/everest_device_model_storage.cpp b/modules/OCPP201/device_model/everest_device_model_storage.cpp new file mode 100644 index 000000000..873fe5ec6 --- /dev/null +++ b/modules/OCPP201/device_model/everest_device_model_storage.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include + +namespace module::device_model { +ocpp::v201::DeviceModelMap EverestDeviceModelStorage::get_device_model() { + return {}; +} + +std::optional +EverestDeviceModelStorage::get_variable_attribute(const ocpp::v201::Component& /*component_id*/, + const ocpp::v201::Variable& /*variable_id*/, + const ocpp::v201::AttributeEnum& /*attribute_enum*/) { + return std::nullopt; +} + +std::vector +EverestDeviceModelStorage::get_variable_attributes(const ocpp::v201::Component& /*component_id*/, + const ocpp::v201::Variable& /*variable_id*/, + const std::optional& /*attribute_enum*/) { + return {}; +} + +bool EverestDeviceModelStorage::set_variable_attribute_value(const ocpp::v201::Component& /*component_id*/, + const ocpp::v201::Variable& /*variable_id*/, + const ocpp::v201::AttributeEnum& /*attribute_enum*/, + const std::string& /*value*/, + const std::string& /*source*/) { + return false; +} + +std::optional +EverestDeviceModelStorage::set_monitoring_data(const ocpp::v201::SetMonitoringData& /*data*/, + const ocpp::v201::VariableMonitorType /*type*/) { + return std::nullopt; +} + +bool EverestDeviceModelStorage::update_monitoring_reference(const int32_t /*monitor_id*/, + const std::string& /*reference_value*/) { + return false; +} + +std::vector +EverestDeviceModelStorage::get_monitoring_data(const std::vector& /*criteria*/, + const ocpp::v201::Component& /*component_id*/, + const ocpp::v201::Variable& /*variable_id*/) { + return {}; +} + +ocpp::v201::ClearMonitoringStatusEnum EverestDeviceModelStorage::clear_variable_monitor(int /*monitor_id*/, + bool /*allow_protected*/) { + return ocpp::v201::ClearMonitoringStatusEnum::NotFound; +} + +int32_t EverestDeviceModelStorage::clear_custom_variable_monitors() { + return 0; +} + +void EverestDeviceModelStorage::check_integrity() { +} +} // namespace module::device_model diff --git a/modules/OCPP201/device_model/everest_device_model_storage.hpp b/modules/OCPP201/device_model/everest_device_model_storage.hpp new file mode 100644 index 000000000..4bbc2847d --- /dev/null +++ b/modules/OCPP201/device_model/everest_device_model_storage.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +namespace module::device_model { +class EverestDeviceModelStorage : public ocpp::v201::DeviceModelStorageInterface { +public: + virtual ~EverestDeviceModelStorage() override = default; + virtual ocpp::v201::DeviceModelMap get_device_model() override; + virtual std::optional + get_variable_attribute(const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum) override; + virtual std::vector + get_variable_attributes(const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id, + const std::optional& attribute_enum) override; + virtual bool set_variable_attribute_value(const ocpp::v201::Component& component_id, + const ocpp::v201::Variable& variable_id, + const ocpp::v201::AttributeEnum& attribute_enum, const std::string& value, + const std::string& source) override; + virtual std::optional + set_monitoring_data(const ocpp::v201::SetMonitoringData& data, const ocpp::v201::VariableMonitorType type) override; + virtual bool update_monitoring_reference(const int32_t monitor_id, const std::string& reference_value) override; + virtual std::vector + get_monitoring_data(const std::vector& criteria, + const ocpp::v201::Component& component_id, const ocpp::v201::Variable& variable_id) override; + virtual ocpp::v201::ClearMonitoringStatusEnum clear_variable_monitor(int monitor_id, bool allow_protected) override; + virtual int32_t clear_custom_variable_monitors() override; + virtual void check_integrity() override; +}; +} // namespace module::device_model diff --git a/modules/OCPP201/doc.rst b/modules/OCPP201/doc.rst index 19fc1d5c6..4aec54fdc 100644 --- a/modules/OCPP201/doc.rst +++ b/modules/OCPP201/doc.rst @@ -1,5 +1,428 @@ -Global Errors -============= +.. _everest_modules_handwritten_OCPP201: -The `enable_global_errors` flag for this module is enabled. This module is therefore able to retrieve and process all reported errors -from other modules loaded in the same EVerest configuration. +OCPP2.0.1 Module +================ + +This module implements and integrates OCPP 2.0.1 within EVerest. A connection to a Charging Station Management System (CSMS) can be +established by loading this module as part of the EVerest configuration. This module leverages `libocpp `_, +EVerest's OCPP library. + +The EVerest config `config-sil-ocpp201.yaml <../../config/config-sil-ocpp201.yaml>`_ serves as an example for how to add the OCPP201 module +to your EVerest config. + +Module configuration +-------------------- + +Like for every EVerest module, the configuration parameters are defined as part of the module `manifest <../manifest.yaml>`_. OCPP2.0.1 defines +a device model structure and a lot of standardized variables that are used within the functional requirements of the protocol. Please see +Part 1 - Architecture & Topology of the OCPP2.0.1 specification for further information about the device model and how it is composed. + +For this module, the device model is configured separately in a JSON format. This module initializes the device model based on the configuration +parameter **DeviceModelConfigPath**. It shall point to the directory where the component configuration files are located in two subdirectories: + +* standardized +* custom + +The `device model setup from libocpp `_ serves as a good example. +The split between the directories only has semantic reasons. The **standardized** directory usually does not need to be modified since it contains +standardized components and variables that the specification refers to in its functional requirements. The **custom** directory is meant to be used +for components that are custom for your specific charging station. Especially the number of EVSE and Connector components, as well as their +variables and values, need to be in line with the physical setup of the charging station. + +Each device model component is represented by a JSON component config file. This config specifies the component and all its variables, +characteristics, attributes, and monitors. Please see `the documentation for the device model initialization +`_ for further information on how it is set up. + +To add a custom component, you can simply add another JSON configuration file for it, and it will automatically be applied and reported. + +Integration in EVerest +---------------------- + +This module leverages **libocpp** ``_, EVerest's OCPP library. Libocpp's approach to implementing the OCPP +protocol is to do as much work as possible as part of the library. It therefore fulfills a large amount of protocol requirements internally. +OCPP is a protocol that affects, controls, and monitors many areas of a charging station's operation though. It is therefore required to +integrate libocpp with other parts of EVerest. This integration is done by this module and will be explained in this section. + +For a detailed description of libocpp and its functionalities, please refer to `its documentation `_. + +The `manifest <../manifest.yaml>`_ of this module defines requirements and implementations of EVerest interfaces to integrate the OCPP communication +with other parts of EVerest. In order to describe how the responsibilities for functions and operations required by OCPP are divided between libocpp +and this module, the following sections pick up the requirements of this module and implementations one by one. + +Provides: auth_validator +^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `auth_token_validator <../../interfaces/auth_token_validator.yaml>`_ + +This interface is implemented to forward authorization requests from EVerest to libocpp. Libocpp contains the business logic to either validate the +authorization request locally using the authorization cache and local authorization list or to forward the request to the CSMS using an +**Authorize.req**. The implementation also covers the validation of Plug&Charge authorization requests. + +Provides: auth_provider +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `auth_token_provider <../../interfaces/auth_token_provider.yaml>`_ + +This interface is implemented to publish authorization requests from the CSMS within EVerest. An authorization request from the CSMS is implemented +by a **RequestStartTransaction.req**. + +Provides: data_transfer +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp_data_transfer <../../interfaces/ocpp_data_transfer.yaml>`_ + +This interface is implemented to provide a command to initiate a **DataTransfer.req** from the charging station to the CSMS. + +Provides: ocpp_generic +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp <../../interfaces/ocpp.yaml>`_ + +This interface is implemented to provide an API to control an OCPP service and to set and get OCPP-specific data. + +Provides: session_cost +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `session_cost <../../interfaces/session_cost.yaml>`_ + +This interface is implemented to publish session costs received by the CSMS as part of the California Pricing whitepaper extension. + +Requires: evse_manager +^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `evse_manager <../../interfaces/evse_manager.yaml>`_ + +Typically the `EvseManager <../EvseManager/>`_ module is used to fulfill this requirement. + +This module requires (1-128) implementations of this interface in order to integrate with the charge control logic of EVerest. One connection represents +one EVSE. In order to manage multiple EVSEs via one OCPP connection, multiple connections need to be configured in the EVerest config file. + +This module makes use of the following commands of this interface: + +* **get_evse** to get the EVSE id of the module implementing the **evse_manager** interface at startup +* **pause_charging** to pause charging in case a **TransactionEvent.conf** indicates charging shall be paused +* **stop_transaction** to stop a transaction in case the CSMS stops a transaction by e.g. a **RequestStopTransaction.req** +* **force_unlock** to force the unlock of a connector in case the CSMS sends a **UnlockConnector.req** +* **enable_disable** to set the EVSE to operative or inoperative, e.g. in case the CSMS sends a **ChangeAvailability.req**. This command can be called from + different sources. It therefore contains an argument **priority** in order to override the status if required. OCPP201 uses a priority of 5000, which is + mid-range. +* **set_external_limits** to apply power or ampere limits at the EVSE received by the CSMS using the SmartCharging feature profile. Libocpp contains the + business logic to calculate the composite schedule for received charging profiles. This module gets notified in case charging profiles are added, + changed, or cleared. When notified, this module requests the composite schedule from libocpp and publishes the result via the + `Provides: ocpp_generic <#provides-ocpp_generic>`_ interface. The duration of the composite schedule can be configured by the configuration parameter + **PublishChargingScheduleDurationS** of this module. The configuration parameter **PublishChargingScheduleIntervalS** defines the interval to use to + periodically retrieve and publish the composite schedules. The configuration parameter **RequestCompositeScheduleUnit** can be used to specify the unit in + which composite schedules are requested and shared within EVerest. +* **set_get_certificate_response** to report that the charging station received a **Get15118EVCertificate.conf** from the CSMS (EV Contract installation +for Plug&Charge) + +The interface is used to receive the following variables: + +* **powermeter** to push powermeter values of an EVSE. Libocpp initiates **MeterValues.req** and **TransactionEvent.req** for meter values internally and is + responsible for complying with the configured intervals and measurands for clock-aligned and sampled meter values. +* **ev_info** to obtain the state of charge (SoC) of an EV. If present, this is reported as part of a **MeterValues.req** +* **limits** to obtain the current offered to the EV. If present, this is reported as part of a **MeterValues.req** +* **session_event** to trigger **StatusNotification.req** and **TransactionEvent.req** based on the reported event. This signal drives the state machine and + the transaction handling of libocpp. +* **iso15118_certificate_request** to trigger a **DataTransfer.req(Get15118EVCertificateRequest)** as part of the Plug&Charge process +* **waiting_for_external_ready** to obtain the information that a module implementing this interface is waiting for an external ready signal +* **ready** to obtain a ready signal from a module implementing this interface + +Requires: connector_zero_sink +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `external_energy_limits <../../interfaces/external_energy_limits.yaml>`_ + +Typically the `EnergyNode <../EnergyNode/>`_ module is used to fulfill this requirement. + +This module optionally requires the connection to a module implementing the **external_energy_limits** interface. This connection is used to apply power or +ampere limits at EVSE id zero received by the CSMS using the SmartCharging feature profile. + +This module makes use of the following commands of this interface: +* **set_external_limits** to apply power or ampere limits at EVSE id zero received by the CSMS using the SmartCharging feature profile. + +Requires: auth +^^^^^^^^^^^^^^ + +**Interface**: `auth <../../interfaces/auth.yaml>`_ + +Typically the `Auth <../Auth/>`_ module is used to fulfill this requirement. + +This module requires a connection to a module implementing the **auth** interface. This connection is used to set the standardized **ConnectionTimeout** +configuration key if configured and/or changed by the CSMS. + +This module makes use of the following commands of this interface: + +* **set_connection_timeout** which is e.g., called in case the CSMS uses a **SetVariables.req(EVConnectionTimeout)** +* **set_master_pass_group_id** which is e.g., called in case the CSMS uses a **SetVariables.req(MastrPassGroupId)** + +Requires: system +^^^^^^^^^^^^^^^^ + +**Interface**: `system <../../interfaces/system.yaml>`_ + +The `System <../System/>`_ module can be used to fulfill this requirement. Note that this module is not meant to be used in production systems without any +modification! + +This module requires a connection to a module implementing the **system** interface. This connection is used to execute and control system-wide operations that +can be triggered by the CSMS, like log uploads, firmware updates, and resets. + +This module makes use of the following commands of this interface: + +* **update_firmware** to forward a **FirmwareUpdate.req** message from the CSMS +* **allow_firmware_installation** to notify the module that the installation of the firmware is now allowed. A prerequisite for this is that all EVSEs are set + to inoperative. This module and libocpp take care of setting the EVSEs to inoperative before calling this command. +* **upload_logs** to forward a **GetLog.req** message from the CSMS +* **is_reset_allowed** to check if a **Reset.req** message from the CSMS shall be accepted or rejected +* **reset** to perform a reset in case of a **Reset.req** message from the CSMS +* **set_system_time** to set the system time communicated by a **BootNotification.conf** or **Heartbeat.conf** messages from the CSMS +* **get_boot_reason** to obtain the boot reason to use it as part of the **BootNotification.req** at startup + +The interface is used to receive the following variables: + +* **log_status** to obtain the log update status. This triggers a **LogStatusNotification.req** message to inform the CSMS about the current status. This signal is + expected as a result of an **upload_logs** command. +* **firmware_update_status** to obtain the firmware update status. This triggers a **FirmwareStatusNotification.req** message to inform the CSMS about the current + status. This signal is expected as a result of an **update_firmware** command. + +Requires: security +^^^^^^^^^^^^^^^^^^ + +**Interface**: `evse_security <../../interfaces/evse_security.yaml>`_ + +This module requires a connection to a module implementing the **evse_security** interface. This connection is used to execute security-related operations and to +manage certificates and private keys. + +Typically the `EvseSecurity <../EvseSecurity/>`_ module is used to fulfill this requirement. + +This module makes use of the following commands of this interface: + +* **install_ca_certificate** to handle an **InstallCertificate.req** message from the CSMS +* **delete_certificate** to handle a **DeleteCertificate.req** message from the CSMS +* **update_leaf_certificate** to handle a **CertificateSigned.req** message from the CSMS +* **verify_certificate** to verify certificates from the CSMS that are sent as part of **UpdateFirmware.req** or to validate the contract certificate used for + Plug&Charge. +* **get_installed_certificates** to handle a **GetInstalledCertificateIds.req** message from the CSMS +* **get_v2g_ocsp_request_data** to update the OCSP cache of V2G sub-CA certificates using **GetCertificateStatus.req**. Triggering this message is handled by + libocpp internally +* **get_mo_ocsp_request_data** to include the **iso15118CertificateHashData** as part of an **Authorize.req** for Plug&Charge if required +* **update_ocsp_cache** to update the OCSP cache, which is part of a **GetCertificateStatus** message from the CSMS +* **is_ca_certificate_installed** to verify if a certain CA certificate is installed +* **generate_certificate_signing_request** to generate a CSR that can be used as part of a **SignCertificate.req** message to the CSMS to generate or update the + SECC or CSMS leaf certificates +* **get_leaf_certificate_info** to get the certificate and private key path of the CSMS client certificate used for security profile 3 +* **get_verify_file** to get the path to a CA bundle that can be used for verifying, e.g., the CSMS TLS server certificate +* **get_leaf_expiry_days_count** to determine when a leaf certificate expires. This information is used by libocpp in order to renew leaf certificates in case + they expire soon + +Note that a lot of conversion between the libocpp types and the generated EVerest types are required for the given commands. Since the +conversion functionality is used by this OCPP201 module and the OCPP1.6 module, it is implemented as a +`separate library <../../lib/staging/ocpp/>`_ . + +Requires: data_transfer +^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `ocpp_data_transfer <../../interfaces/ocpp_data_transfer.yaml>`_ + +This module optionally requires a connection to a module implementing the **ocpp_data_transfer** interface. This connection is used to handle **DataTransfer.req** +messages from the CSMS. A module implementing this interface can contain custom logic to handle the requests from the CSMS. + +This module makes use of the following commands of this interface: + +* **data_transfer** to forward **DataTransfer.req** messages from the CSMS + +Requires: display_message +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Interface**: `display_message <../../interfaces/display_message.yaml>`_ + +This module optionally requires a connection to a module implementing the **display_message** interface. This connection is used to allow the CSMS to display pricing +or other information on the display of a charging station. In order to fulfill the requirements of the California Pricing whitepaper, it is required to connect a +module implementing this interface. + +This module makes use of the following commands of this interface: + +* **set_display_message** to set a message on the charging station's display. This is executed when the CSMS sends a **SetDisplayMessage.req** or **TransactionEvent.conf** + (including cost and tariff data) message to the charging station. +* **get_display_messages** to forward a **GetDisplayMessage.req** from the CSMS +* **clear_display_message** to forward a **ClearDisplayMessage.req** from the CSMS + +Error Handling +-------------- + +The **enable_global_errors** flag for this module is true in its manifest. This module is +therefore able to retrieve and process all reported errors from other +modules that are loaded in the same EVerest configuration. + +The error reporting via OCPP2.0.1 follows the Minimum Required Error Codes (MRECS): https://inl.gov/chargex/mrec/ . This proposes a unified methodology +to define and classify a minimum required set of error codes and how to report them via OCPP2.0.1. + +StatusNotification +^^^^^^^^^^^^^^^^^^ + +In contrast to OCPP1.6, error information is not transmitted as part of the StatusNotification.req. +A **StatusNotification.req** with status **Faulted** will be set to faulted only in case the error received is of the special type **evse_manager/Inoperative**. +This indicates that the EVSE is inoperative (not ready for energy transfer). + +In OCPP2.0.1 errors can be reported using the **NotifyEventRequest.req**. This message is used to report all other errros received. + +Current Limitation +^^^^^^^^^^^^^^^^^^ + +In OCPP2.0.1 errors can be reported using the **NotifyEventRequest** +message. The **eventData** property carries the relevant information. + +This format of reporting errors deviates from the mechanism used within +EVerest. This data structure forces to map an error to a +component-variable combination. This requires a mapping +mechanism between EVerest errors and component-variable +combination. + +Currently this module maps the Error to one of these three Components: + +* ChargingStation (if error.origin.mapping.evse is not set or 0) +* EVSE (error.origin.mapping.evse is set and error.origin.mapping.connector is not set) +* Connector (error.origin.mapping.evse is set and error.origin.mapping.connector is set) + +The Variable used as part of the NotifyEventRequest is constantly defined to **Problem** for now. + +The goal is to have a more advanced mapping of reported errors to the respective component-variable combinations in the future. + +Certificate Management +---------------------- + +Two leaf certificates are managed by the OCPP communication enabled by this module: + +* CSMS Leaf certificate (used for mTLS for SecurityProfile3) +* SECC Leaf certificate (Server certificate for ISO15118) + +60 seconds after the first **BootNotification.req** message has been accepted by the CSMS, the charging station will check if the existing +certificates are not present or have been expired. If this is the case, the charging station initiates the process of requesting a new +certificate by sending a certificate signing request to CSMS. + +For the CSMS Leaf certificate, this process is only triggered if SecurityProfile 3 is used. + +For the SECC Leaf certificate, this process is only triggered if Plug&Charge is enabled by setting the **ISO15118PnCEnabled** to **true**. + +If a certificate has expired is then periodically checked every 12 hours. + +In addition to that, the charging station periodically updates the OCSP responses of the sub-CA certificates of the V2G certificate chain. +The OCSP response is cached and can be used as part of the ISO15118 TLS handshake with EVs. The OCSP update is by default performed +every seven days. The timestamp of the last update is stored persistently, so that this process is not necessarily performed +at every start up. + +Energy Management and Smart Charging Integration +------------------------------------------------ + +OCPP2.0.1 defines the SmartCharging feature profile to allow the CSMS to control or influence the power consumption of the charging station. +This module integrates the composite schedule(s) within EVerest's energy management. For further information about smart charging and the +composite schedule calculation please refer to the OCPP2.0.1 specification. + +The integration of the composite schedules is implemented through the optional requirement(s) `evse_energy_sink` (interface: `external_energy_limits`) +of this module. Depending on the number of EVSEs configured, each composite limit is communicated via a seperate sink, including the composite schedule +for EVSE with id 0 (representing the whole charging station). The easiest way to explain this is with an example. If your charging station +has two EVSEs you need to connect three modules that implement the `external_energy_limits` interface: One representing evse id 0 and +two representing your actual EVSEs. + +📌 **Note:** You have to configure an evse mapping for each module connected via the evse_energy_sink connection. This allows the module to identify +which requirement to use when communicating the limits for the EVSEs. For more information about the module mapping please see +`3-tier module mappings `_. + +This module defines a callback that gets executed every time charging profiles are changed, added or removed by the CSMS. The callback retrieves +the composite schedules for all EVSEs (including evse id 0) and calls the `set_external_limits` command of the respective requirement that implements +the `external_energy_limits` interface. In addition, the config parameter `CompositeScheduleIntervalS` defines a periodic interval to retrieve +the composite schedule also in case no charging profiles have been changed. The configuration parameter `RequestCompositeScheduleDurationS` defines +the duration in seconds of the requested composite schedules starting now. The value configured for `RequestCompositeScheduleDurationS` shall be greater +than the value configured for `CompositeScheduleIntervalS` because otherwise time periods could be missed by the application. + +Device model implementation details +----------------------------------- + +For managing configuration and telemetry data of a charging station, the OCPP2.0.1 specification introduces +a device model that is very different to the design of OCPP1.6. +The specified device model comes with these high-level requirements: + +* 3-tier model: Break charging station down into 3 main tiers: ChargingStation, EVSE and Connector +* Components and Variables: Break down charging station into components and variables for configuration and telemetry +* Complex data structure for reporting and configuration of variables +* Device model contains variables of the whole charging station, beyond OCPP business logic + +The device model of OCPP2.0.1 can contain various physical or logical components and +variables. While in OCPP1.6 almost all of the standardized configuration keys are used to influence the control flow of +libocpp, in OCPP2.0.1 the configuration and telemetry variables that can be part of the device model go beyond the +control or reporting capabilities of only libocpp. Still there is a large share of standardized variables in OCPP2.0.1 +that do influence the control flow of libocpp. + +Internally and externally managed variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +EVerest has multiple different data sources that control the values variables that OCPP requires to report to the CSMS. +It is therefore required to make a distinction between **internally** and **externally** managed variables of the device model. + +We define **internally** and **externally** managed variables as follows: + +* Internally Managed: Owned, stored and accessed in libocpp in device model storage + Examples: HeartbeatInterval, AuthorizeRemoteStart, SampledDataTxEndedMeasurands, AuthCacheStorage +* Externally Managed: Owned, stored and accessed via EVerest config service (not yet supported) + Examples: ConnectionTimeout, MasterPassGroupId +* For externally managed variables a mapping to the EVerest configuration parameter is defined (not yet supported) + +Note that the EVerest config service is not yet implemented. Currently all components and variables are controlled +by the libocpp device model storage implementation. + +Device Model Implementation of this module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This module provides an implementation of device model API provided as part of libocpp (it implements +`device_model_storage_interface.hpp`). +The implementation is designed to fullfill the requirements of the device model API even if the components and variables are +controlled by different sources (Internally, Externally). + +Device Model Sources +^^^^^^^^^^^^^^^^^^^^ + +Device Model variables are defined in JSON component configs. For each variable a property `source` can be used to define +the source that controls it. This design allows for a single source of truth for each variable and it +allows the device model implementation of this module to address the correct source for the requested operation. +Today `OCPP` is the only supported source for internally managed variables. + +Sources for externally managed configuration variables like the EVerest config service are under development. + +Sequence of variable access for internally and externally managed variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: doc/sequence_config_service_and_ocpp.png + +Class diagram for device model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. image:: doc/device_model_class_diagram.png + +Clarification of the device model classes of this diagram: + +* DeviceModel: + + * Part of libocpp + * Contains device model representation and business logic to prevalidate requests to the device model variables + * Contains reference to device model interface implementation + +* DeviceModelStorageInterface: + + * Pure virtual class of libocpp + * Defines contract for device model implementations + +* DeviceModelStorageSqlite + + * Implements DeviceModelStorageInterface as part of libocpp + * This storage holds internally managed variables + +* EverestDeviceModelStorage + + * Implements DeviceModelStorageInterface as part of everest-core (OCPP201 module) + * Uses EVerest config service to retrieve configuration variables of EVerest modules + +* ComposedDeviceModelStorage + + * (Final) implementation of DeviceModelStorageInterface as part of everest-core (OCPP201 module) + * A reference of this class will be passed to libocpp's ChargePoint constructor + * Differentiates between externally and internally managed variables diff --git a/modules/OCPP201/doc/device_model_class_diagram.png b/modules/OCPP201/doc/device_model_class_diagram.png new file mode 100644 index 000000000..1b989dab3 Binary files /dev/null and b/modules/OCPP201/doc/device_model_class_diagram.png differ diff --git a/modules/OCPP201/doc/device_model_class_diagram.puml b/modules/OCPP201/doc/device_model_class_diagram.puml new file mode 100644 index 000000000..bca34cbf7 --- /dev/null +++ b/modules/OCPP201/doc/device_model_class_diagram.puml @@ -0,0 +1,49 @@ +@startuml + +package libocpp { + +class ChargePoint { + - device_model: DeviceModel +} + +class DeviceModel { + - device_model: DeviceModelStorageInterface + + get_device_model(): DeviceModelRepresentation + + get_value(...): T + + set_value(...): SetVariableStatusEnum +} + +interface DeviceModelStorageInterface { + + get_device_model(): DeviceModelRepresentation + + get_variable_attribute(...): std::optional + + set_variable_attribute_value(...): bool +} + +class DeviceModelStorageSqlite implements DeviceModelStorageInterface + +} + +package everest-core { + +class EverestDeviceModelStorage implements libocpp.DeviceModelStorageInterface +class ComposedDeviceModelStorage implements libocpp.DeviceModelStorageInterface { + - everest_storage: EverestDeviceModelStorage + - libocpp_storage: DeviceModelStorageSqlite +} +} + +note left of ChargePoint + ChargePoint and DeviceModel are + implemented within the library. +end note + +note right of ComposedDeviceModelStorage + This implementation will be passed to libocpp's constructor +end note + +ChargePoint *-- DeviceModel +DeviceModel *-- DeviceModelStorageInterface +ComposedDeviceModelStorage *-- EverestDeviceModelStorage +ComposedDeviceModelStorage *-- DeviceModelStorageSqlite + +@enduml diff --git a/modules/OCPP201/doc/sequence_config_service_and_ocpp.png b/modules/OCPP201/doc/sequence_config_service_and_ocpp.png new file mode 100644 index 000000000..7e0b87995 Binary files /dev/null and b/modules/OCPP201/doc/sequence_config_service_and_ocpp.png differ diff --git a/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml b/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml new file mode 100644 index 000000000..b32528b25 --- /dev/null +++ b/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml @@ -0,0 +1,46 @@ +@startuml +'https://plantuml.com/sequence-diagram +!pragma teoz true +participant CSMS order 10 +participant libocpp order 20 +participant ComposedDeviceModel order 30 +database DeviceModelStorageSqlite order 40 +database EverestDeviceModelStorage order 50 + +autonumber "" +skinparam sequenceArrowThickness 2 + +== Get Device Model at startup == + +ComposedDeviceModel->ComposedDeviceModel: Initialize device model based on component config +libocpp->ComposedDeviceModel: get_device_model +loop For each variable defined in component config + alt internally managed variable + ComposedDeviceModel->InternalStorage: get_value + InternalStorage->ComposedDeviceModel: get_value response + else externally managed variable + ComposedDeviceModel->ExternalStorage: get_value + ExternalStorage->ComposedDeviceModel: get_value response + end +end +ComposedDeviceModel->libocpp: get_device_model response + +== SetVariables.req by CSMS == +CSMS->libocpp: SetVariables.req +loop For each SetVariable request + libocpp->libocpp: Logical internal validation + libocpp->libocpp: Device Model validation + alt request is valid + libocpp->ComposedDeviceModel: set_value + alt internally managed variable + ComposedDeviceModel->InternalStorage: set_value + InternalStorage->ComposedDeviceModel: set_value response + else externally managed variable + ComposedDeviceModel->ExternalStorage: set_value + ExternalStorage->ComposedDeviceModel: set_value response + end + ComposedDeviceModel->libocpp: set_value response + end +end + +@enduml diff --git a/modules/OCPP201/error_handling.hpp b/modules/OCPP201/error_handling.hpp new file mode 100644 index 000000000..91226ac71 --- /dev/null +++ b/modules/OCPP201/error_handling.hpp @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef OCPP201_ERROR_HANDLING_HPP +#define OCPP201_ERROR_HANDLING_HPP + +#include +#include +#include + +#include + +namespace module { +const std::unordered_map MREC_ERROR_MAP = { + {"connector_lock/MREC1ConnectorLockFailure", "CX001"}, + {"evse_board_support/MREC2GroundFailure", "CX002"}, + {"evse_board_support/MREC3HighTemperature", "CX003"}, + {"evse_board_support/MREC4OverCurrentFailure", "CX004"}, + {"evse_board_support/MREC5OverVoltage", "CX005"}, + {"evse_board_support/MREC6UnderVoltage", "CX006"}, + {"evse_board_support/MREC8EmergencyStop", "CX008"}, + {"evse_board_support/MREC10InvalidVehicleMode", "CX010"}, + {"evse_board_support/MREC14PilotFault", "CX014"}, + {"evse_board_support/MREC15PowerLoss", "CX015"}, + {"evse_board_support/MREC17EVSEContactorFault", "CX017"}, + {"evse_board_support/MREC18CableOverTempDerate", "CX018"}, + {"evse_board_support/MREC19CableOverTempStop", "CX019"}, + {"evse_board_support/MREC20PartialInsertion", "CX020"}, + {"evse_board_support/MREC23ProximityFault", "CX023"}, + {"evse_board_support/MREC24ConnectorVoltageHigh", "CX024"}, + {"evse_board_support/MREC25BrokenLatch", "CX025"}, + {"evse_board_support/MREC26CutCable", "CX026"}, + {"evse_manager/MREC4OverCurrentFailure", "CX004"}, + {"ac_rcd/MREC2GroundFailure", "CX002"}, +}; + +const auto EVSE_MANAGER_INOPERATIVE_ERROR = "evse_manager/Inoperative"; +const auto CHARGING_STATION_COMPONENT_NAME = "ChargingStation"; +const auto EVSE_COMPONENT_NAME = "EVSE"; +const auto CONNECTOR_COMPONENT_NAME = "Connector"; +const auto PROBLEM_VARIABLE_NAME = "Problem"; + +/// \brief Derives the EventData from the given \p error, \p cleared and \p event_id parameters +ocpp::v201::EventData get_event_data(const Everest::error::Error& error, const bool cleared, const int32_t event_id) { + ocpp::v201::EventData event_data; + event_data.eventId = event_id; // This can theoretically conflict with eventIds generated in libocpp (e.g. + // for monitoring events), but the spec does not strictly forbid that + event_data.timestamp = ocpp::DateTime(error.timestamp); + event_data.trigger = ocpp::v201::EventTriggerEnum::Alerting; + event_data.cause = std::nullopt; // TODO: use caused_by when available within error object + event_data.actualValue = cleared ? "false" : "true"; + + if (MREC_ERROR_MAP.count(error.type)) { + event_data.techCode = MREC_ERROR_MAP.at(error.type); + } else { + event_data.techCode = error.type; + } + + event_data.techInfo = error.description; + event_data.cleared = cleared; + event_data.transactionId = std::nullopt; // TODO: Do we need to set this here? + event_data.variableMonitoringId = std::nullopt; // We dont need to set this for HardwiredNotification + event_data.eventNotificationType = ocpp::v201::EventNotificationEnum::HardWiredNotification; + + // TODO: choose proper component + const auto evse_id = error.origin.mapping.has_value() ? error.origin.mapping.value().evse : 0; + if (evse_id == 0) { + // component is ChargingStation + event_data.component = {CHARGING_STATION_COMPONENT_NAME}; // TODO: use origin of error for mapping to component? + } else if (not error.origin.mapping.value().connector.has_value()) { + // component is EVSE + ocpp::v201::EVSE evse = {evse_id}; + event_data.component = {EVSE_COMPONENT_NAME, std::nullopt, evse}; + } else { + // component is Connector + ocpp::v201::EVSE evse = {evse_id, std::nullopt, error.origin.mapping.value().connector.value()}; + event_data.component = {CONNECTOR_COMPONENT_NAME, std::nullopt, evse}; + } + event_data.variable = {PROBLEM_VARIABLE_NAME}; // TODO: use type of error for mapping to variable? + return event_data; +} +}; // namespace module + +#endif // OCPP201_ERROR_HANDLING_HPP diff --git a/modules/OCPP201/main/emptyImpl.cpp b/modules/OCPP201/main/emptyImpl.cpp deleted file mode 100644 index e69326b6c..000000000 --- a/modules/OCPP201/main/emptyImpl.cpp +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#include "emptyImpl.hpp" - -namespace module { -namespace main { - -void emptyImpl::init() { -} - -void emptyImpl::ready() { -} - -} // namespace main -} // namespace module diff --git a/modules/OCPP201/main/emptyImpl.hpp b/modules/OCPP201/main/emptyImpl.hpp deleted file mode 100644 index 9755f165d..000000000 --- a/modules/OCPP201/main/emptyImpl.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest -#ifndef MAIN_EMPTY_IMPL_HPP -#define MAIN_EMPTY_IMPL_HPP - -// -// AUTO GENERATED - MARKED REGIONS WILL BE KEPT -// template version 3 -// - -#include - -#include "../OCPP201.hpp" - -// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 -// insert your custom include headers here -// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 - -namespace module { -namespace main { - -struct Conf {}; - -class emptyImpl : public emptyImplBase { -public: - emptyImpl() = delete; - emptyImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - emptyImplBase(ev, "main"), mod(mod), config(config){}; - - // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 - // insert your public definitions here - // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 - -protected: - // no commands defined for this interface - - // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 - // insert your protected definitions here - // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 - -private: - const Everest::PtrContainer& mod; - const Conf& config; - - virtual void init() override; - virtual void ready() override; - - // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 - // insert your private definitions here - // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 -}; - -// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 -// insert other definitions here -// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 - -} // namespace main -} // namespace module - -#endif // MAIN_EMPTY_IMPL_HPP diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index 126267b11..51df9abb6 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -1,11 +1,12 @@ description: A OCPP charge point / charging station module for OCPP 2.0.1 config: MessageLogPath: - description: Path to folder where logs of all OCPP messages get written to + description: Path to directory where logs of all OCPP messages are written to type: string default: /tmp/everest_ocpp_logs CoreDatabasePath: - description: Path to the persistent SQLite database folder + description: Path to the persistent SQLite database directory. Please refer to the libocpp documentation for more information + about the database and its structure. type: string default: /tmp/ocpp201 DeviceModelDatabasePath: @@ -17,15 +18,17 @@ config: type: string default: device_model_migrations DeviceModelConfigPath: - description: Path to the device model component config directory + description: Path to the device model component config directory. Libocpp defines a certain schema for these files. Please refer to the documentation + of libocpp for more information about the configuration options. type: string default: component_config EnableExternalWebsocketControl: - description: If true websocket can be disconnected and connected externally + description: If true websocket can be disconnected and connected externally. This parameter is for debug and testing purposes. type: boolean default: false MessageQueueResumeDelay: - description: Time (seconds) to delay resuming the message queue after reconnecting + description: Time (seconds) to delay resuming the message queue after reconnecting. This parameter was introduced because + some OCTT test cases require that the first message after a reconnect is sent by the CSMS. type: integer default: 0 CompositeScheduleIntervalS: @@ -51,9 +54,6 @@ config: type: string default: 'A' provides: - main: - description: This is a OCPP 2.0.1 charge point - interface: empty auth_validator: description: Validates the provided token using CSMS, AuthorizationList or AuthorizationCache interface: auth_token_validator @@ -90,14 +90,18 @@ requires: interface: auth min_connections: 1 max_connections: 1 - connector_zero_sink: + evse_energy_sink: interface: external_energy_limits min_connections: 0 - max_connections: 1 + max_connections: 129 display_message: 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.cpp b/modules/OCPP201/ocpp_generic/ocppImpl.cpp index f3af1d42a..4967c2400 100644 --- a/modules/OCPP201/ocpp_generic/ocppImpl.cpp +++ b/modules/OCPP201/ocpp_generic/ocppImpl.cpp @@ -3,6 +3,7 @@ #include "ocppImpl.hpp" #include +#include namespace module { namespace ocpp_generic { @@ -26,11 +27,7 @@ bool ocppImpl::handle_restart() { void ocppImpl::handle_security_event(types::ocpp::SecurityEvent& event) { std::optional timestamp; if (event.timestamp.has_value()) { - try { - timestamp = ocpp::DateTime(event.timestamp.value()); - } catch (...) { - EVLOG_warning << "Timestamp in security event could not be parsed, using current datetime."; - } + timestamp = ocpp_conversions::to_ocpp_datetime_or_now(event.timestamp.value()); } this->mod->charge_point->on_security_event(event.type, event.info, event.critical, timestamp); } 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/OCPP201/transaction_handler.hpp b/modules/OCPP201/transaction_handler.hpp index 86cbf6a10..414a8b8a0 100644 --- a/modules/OCPP201/transaction_handler.hpp +++ b/modules/OCPP201/transaction_handler.hpp @@ -90,6 +90,8 @@ struct TxStartStopConditions { case TxEvent::IMMEDIATE_RESET: is_immediate_reset = true; break; + case TxEvent::NONE: + break; } }; @@ -127,9 +129,10 @@ struct TxStartStopConditions { return !is_authorized or !is_ev_connected; case TxStartStopPoint::EnergyTransfer: return !is_energy_transfered; - default: + case TxStartStopPoint::DataSigned: return false; } + return false; }; }; diff --git a/modules/PN532TokenProvider/CMakeLists.txt b/modules/PN532TokenProvider/CMakeLists.txt index 15061145a..e67bcaa0a 100644 --- a/modules/PN532TokenProvider/CMakeLists.txt +++ b/modules/PN532TokenProvider/CMakeLists.txt @@ -18,6 +18,7 @@ target_include_directories(${MODULE_NAME} target_link_libraries(${MODULE_NAME} PRIVATE + everest::staging::helpers pn532_serial ) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 diff --git a/modules/PN532TokenProvider/main/auth_token_providerImpl.cpp b/modules/PN532TokenProvider/main/auth_token_providerImpl.cpp index 7391069f0..f6142ef25 100644 --- a/modules/PN532TokenProvider/main/auth_token_providerImpl.cpp +++ b/modules/PN532TokenProvider/main/auth_token_providerImpl.cpp @@ -1,8 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -// Copyright 2022 - 2022 Pionix GmbH and Contributors to EVerest +// Copyright Pionix GmbH and Contributors to EVerest #include "auth_token_providerImpl.hpp" +#include + +#include + namespace module { namespace main { @@ -13,8 +17,13 @@ void auth_token_providerImpl::init() { EVLOG_info << "Serial port: " << config.serial_port << " baud rate: " << config.baud_rate; } if (!serial.openDevice(config.serial_port.c_str(), config.baud_rate)) { - EVLOG_AND_THROW(EVEXCEPTION(Everest::EverestConfigError, "Could not open serial port ", config.serial_port, - " with baud rate ", config.baud_rate)); + if (!this->error_state_monitor->is_error_active("generic/CommunicationFault", "Communication timed out")) { + auto error_message = + fmt::format("Could not open serial port {} with baud rate {}", config.serial_port, config.baud_rate); + auto error = this->error_factory->create_error("generic/CommunicationFault", "Communication timed out", + error_message); + raise_error(error); + } return; } } @@ -63,7 +72,8 @@ void auth_token_providerImpl::ready() { provided_token.id_token = {entry.getNFCID(), types::authorization::IdTokenType::ISO14443}; provided_token.authorization_type = types::authorization::AuthorizationType::RFID; if (config.debug) { - EVLOG_info << "Publishing new rfid/nfc token: " << provided_token; + EVLOG_info << "Publishing new rfid/nfc token: " + << everest::staging::helpers::redact(provided_token); } this->publish_provided_token(provided_token); } 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/PhyVersoBSP/system_specific_data_1/generic_arrayImpl.cpp b/modules/PhyVersoBSP/system_specific_data_1/generic_arrayImpl.cpp index 76783df3e..b62fc8617 100644 --- a/modules/PhyVersoBSP/system_specific_data_1/generic_arrayImpl.cpp +++ b/modules/PhyVersoBSP/system_specific_data_1/generic_arrayImpl.cpp @@ -10,7 +10,7 @@ void generic_arrayImpl::init() { mod->serial.signal_opaque_data.connect([this](int connector, const std::vector& data) { if (connector != 1) return; - EVLOG_info << "Received data from " << connector; + EVLOG_debug << "Received data from " << connector; publish_vector_of_ints({data}); }); } diff --git a/modules/PhyVersoBSP/system_specific_data_2/generic_arrayImpl.cpp b/modules/PhyVersoBSP/system_specific_data_2/generic_arrayImpl.cpp index 7aef2252e..0acf64fd5 100644 --- a/modules/PhyVersoBSP/system_specific_data_2/generic_arrayImpl.cpp +++ b/modules/PhyVersoBSP/system_specific_data_2/generic_arrayImpl.cpp @@ -10,7 +10,7 @@ void generic_arrayImpl::init() { mod->serial.signal_opaque_data.connect([this](int connector, const std::vector& data) { if (connector != 2) return; - EVLOG_info << "Received data from " << connector; + EVLOG_debug << "Received data from " << connector; publish_vector_of_ints({data}); }); } diff --git a/modules/PowermeterBSM/CMakeLists.txt b/modules/PowermeterBSM/CMakeLists.txt deleted file mode 100644 index 9830ac39c..000000000 --- a/modules/PowermeterBSM/CMakeLists.txt +++ /dev/null @@ -1,49 +0,0 @@ -# -# AUTO GENERATED - MARKED REGIONS WILL BE KEPT -# template version 3 -# - -# module setup: -# - ${MODULE_NAME}: module name -ev_setup_cpp_module() - -# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 -# insert your custom targets and additional config variables here -# -# this does not create any archive and is just an internal test helper. -add_library(sunspec_framework_object_lib - OBJECT - lib/protocol_related_types.cpp - lib/transport.cpp - lib/known_model.cpp - ) - -target_include_directories( sunspec_framework_object_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) -target_include_directories( sunspec_framework_object_lib PUBLIC ${CMAKE_BINARY_DIR}/generated/include ) -add_dependencies( sunspec_framework_object_lib generate_cpp_files ) - -target_link_libraries( sunspec_framework_object_lib - PUBLIC - everest::modbus - everest::framework - ) - -if (BUILD_DEV_TESTS) - add_subdirectory( tests ) -endif() - -# -# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 - -target_sources(${MODULE_NAME} - PRIVATE - "main/powermeterImpl.cpp" - "ac_meter/sunspec_ac_meterImpl.cpp" -) - -# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 -# insert other things like install cmds etc here -target_link_libraries( ${MODULE_NAME} - PRIVATE - sunspec_framework_object_lib ) -# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/PowermeterBSM/PowermeterBSM.cpp b/modules/PowermeterBSM/PowermeterBSM.cpp deleted file mode 100644 index 18f4dba6d..000000000 --- a/modules/PowermeterBSM/PowermeterBSM.cpp +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest -#include "PowermeterBSM.hpp" - -namespace module { - -void PowermeterBSM::init() { - - read_config(); - dump_config_to_log(); - - invoke_init(*p_main); - invoke_init(*p_ac_meter); -} - -void PowermeterBSM::ready() { - invoke_ready(*p_main); - invoke_ready(*p_ac_meter); -} - -////////////////////////////////////////////////////////////////////// -// -// module module implementation helper - -void PowermeterBSM::read_config() { - - m_serial_device_name = config.serial_device; - m_update_interval = std::chrono::seconds(config.update_interval); - m_watchdog_interval = std::chrono::seconds(config.watchdog_wakeup_interval); - m_unit_id = config.power_unit_id; - m_modbus_base_address = protocol_related_types::ModbusRegisterAddress(config.sunspec_base_address); -} - -void PowermeterBSM::dump_config_to_log() { - EVLOG_verbose << __PRETTY_FUNCTION__ << "\n" - << " module config :\n" - << " device : " << m_serial_device_name << "\n" - << " update interval : " << std::to_string(m_update_interval.count()) << "\n" - << " watchdog interval : " << std::to_string(m_watchdog_interval.count()) << "\n" - << " modbus unit id : " << std::to_string(m_unit_id) << "\n" - << " baud for modbus : " << std::to_string(config.baud) << "\n" - << " use SerialCommHub : " << std::boolalpha << config.use_serial_comm_hub << "\n" - << std::endl; -} - -everest::connection::SerialDeviceConfiguration::BaudRate baudrate_from_integer(int baudrate) { - switch (baudrate) { - case 1200: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_1200; - break; - case 2400: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_2400; - break; - case 4800: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_4800; - break; - case 9600: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_9600; - break; - case 19200: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_19200; - break; - case 38400: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_38400; - break; - case 57600: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_57600; - break; - case 115200: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_115200; - break; - case 230400: - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_230400; - break; - default: - EVLOG_error << "Error setting up connection to serialport: Undefined baudrate! Defaulting to 9600.\n"; - return everest::connection::SerialDeviceConfiguration::BaudRate::Baud_9600; - break; - } -} - -transport::AbstractDataTransport::Spt PowermeterBSM::get_data_transport() { - - if (not m_transport_spt) { - if (config.use_serial_comm_hub) { - if (r_serial_com_0_connection.empty()) { - throw std::runtime_error(""s + __PRETTY_FUNCTION__ + - " SerialCommHub enabled, but no connection available."); - } - m_transport_spt = std::make_shared((*(r_serial_com_0_connection.at(0))), - m_unit_id, m_modbus_base_address); - } else { - auto modbus_transport_spt = - std::make_shared(m_serial_device_name, m_unit_id, m_modbus_base_address); - modbus_transport_spt->serial_device().get_serial_device_config().set_baud_rate( - baudrate_from_integer(config.baud)); - m_transport_spt = modbus_transport_spt; - } - } - return m_transport_spt; -} - -} // namespace module diff --git a/modules/PowermeterBSM/PowermeterBSM.hpp b/modules/PowermeterBSM/PowermeterBSM.hpp deleted file mode 100644 index 5dcabc26e..000000000 --- a/modules/PowermeterBSM/PowermeterBSM.hpp +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest -#ifndef POWERMETER_BSM_HPP -#define POWERMETER_BSM_HPP - -// -// AUTO GENERATED - MARKED REGIONS WILL BE KEPT -// template version 2 -// - -#include "ld-ev.hpp" - -// headers for provided interface implementations -#include -#include - -// headers for required interface implementations -#include - -// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 -// insert your custom include headers here -#include "lib/transport.hpp" -// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 - -namespace module { - -struct Conf { - int power_unit_id; - int sunspec_base_address; - int update_interval; - int watchdog_wakeup_interval; - std::string serial_device; - int baud; - bool use_serial_comm_hub; - std::string meter_id; -}; - -class PowermeterBSM : public Everest::ModuleBase { -public: - PowermeterBSM() = delete; - PowermeterBSM(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, - std::unique_ptr p_main, std::unique_ptr p_ac_meter, - std::vector> r_serial_com_0_connection, Conf& config) : - ModuleBase(info), - mqtt(mqtt_provider), - p_main(std::move(p_main)), - p_ac_meter(std::move(p_ac_meter)), - r_serial_com_0_connection(std::move(r_serial_com_0_connection)), - config(config){}; - - Everest::MqttProvider& mqtt; - const std::unique_ptr p_main; - const std::unique_ptr p_ac_meter; - const std::vector> r_serial_com_0_connection; - const Conf& config; - - // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 - // insert your public definitions here - transport::AbstractDataTransport::Spt get_data_transport(); - std::recursive_mutex& get_device_mutex() { - return m_device_mutex; - } - std::chrono::seconds get_update_interval() const { - return m_update_interval; - } - std::chrono::seconds get_watchdog_interval() const { - return m_watchdog_interval; - } - void dump_config_to_log(); - // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 - -protected: - // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 - // insert your protected definitions here - - std::chrono::seconds m_module_start_timestamp; - - // stuff for transport - protocol_related_types::ModbusUnitId m_unit_id{}; - std::string m_serial_device_name; - protocol_related_types::ModbusRegisterAddress m_modbus_base_address{40000}; - - // variable publish interval - std::chrono::seconds m_update_interval; - - // interval for watchdog variable thread - std::chrono::seconds m_watchdog_interval; - - // implementations have to lock this mutex before accessing the transport. - std::recursive_mutex m_device_mutex; - - void read_config(); - - transport::AbstractDataTransport::Spt m_transport_spt; - // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 - -private: - friend class LdEverest; - void init(); - void ready(); - - // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 - // insert your private definitions here - // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 -}; - -// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 -// insert other definitions here -// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 - -} // namespace module - -#endif // POWERMETER_BSM_HPP diff --git a/modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.cpp b/modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.cpp deleted file mode 100644 index 8273223af..000000000 --- a/modules/PowermeterBSM/ac_meter/sunspec_ac_meterImpl.cpp +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#include "sunspec_ac_meterImpl.hpp" - -namespace module { -namespace ac_meter { - -void sunspec_ac_meterImpl::init() { -} - -void sunspec_ac_meterImpl::ready() { -} - -types::sunspec_ac_meter::Result sunspec_ac_meterImpl::handle_get_sunspec_ac_meter_value(std::string& auth_token) { - // your code for cmd get_sunspec_ac_meter_value goes here - EVLOG_verbose << __PRETTY_FUNCTION__ << " start " << std::endl; - - types::sunspec_ac_meter::Result result{}; - - try { - - std::scoped_lock lock(mod->get_device_mutex()); - - transport::AbstractDataTransport::Spt transport = mod->get_data_transport(); - - transport::DataVector data = transport->fetch(known_model::Sunspec_ACMeter); - - sunspec_model::ACMeter acmeter(data); - - result.A = acmeter.A(); - result.AphA = acmeter.AphA(); - result.AphB = acmeter.AphB(); - result.AphC = acmeter.AphC(); - result.A_SF = acmeter.A_SF(); - result.PhVphA = acmeter.PhVphA(); - result.PhVphB = acmeter.PhVphB(); - result.PhVphC = acmeter.PhVphC(); - result.V_SF = acmeter.V_SF(); - result.Hz = acmeter.Hz(); - result.Hz_SF = acmeter.Hz_SF(); - result.W = acmeter.W(); - result.WphA = acmeter.WphA(); - result.WphB = acmeter.WphB(); - result.WphC = acmeter.WphC(); - result.W_SF = acmeter.W_SF(); - result.VA = acmeter.VA(); - result.VAphA = acmeter.VAphA(); - result.VAphB = acmeter.VAphB(); - result.VAphC = acmeter.VAphC(); - result.VA_SF = acmeter.VAR_SF(); - result.VAR = acmeter.VAR(); - result.VARphA = acmeter.VARphA(); - result.VARphB = acmeter.VARphB(); - result.VARphC = acmeter.VARphC(); - result.VAR_SF = acmeter.VAR_SF(); - result.PFphA = acmeter.PFphA(); - result.PFphB = acmeter.PFphB(); - result.PFphC = acmeter.PFphC(); - result.PF_SF = acmeter.PF_SF(); - result.TotWhIm = acmeter.TotWhIm(); - result.TotWh_SF = acmeter.TotWh_SF(); - result.Evt = acmeter.Evt(); - - } catch (const std::runtime_error& e) { - EVLOG_error << __PRETTY_FUNCTION__ << " Error: " << e.what() << std::endl; - } - - return result; -}; - -} // namespace ac_meter -} // namespace module diff --git a/modules/PowermeterBSM/lib/BSMSnapshotModel.hpp b/modules/PowermeterBSM/lib/BSMSnapshotModel.hpp deleted file mode 100644 index c3474fee2..000000000 --- a/modules/PowermeterBSM/lib/BSMSnapshotModel.hpp +++ /dev/null @@ -1,243 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_BSMSNAPSHOTMODEL_HPP -#define POWERMETER_BSM_BSMSNAPSHOTMODEL_HPP - -#include "sunspec_base.hpp" - -namespace bsm { - -// clang-format off -inline constexpr PointInitializerList BSMSnapshotPointInitList = { - - {"Type" , PointType::enum16 }, - {"Status" , PointType::enum16 }, - {"RCR" , PointType::acc32 }, - {"TotWhImp" , PointType::acc32 }, - {"Wh_SF" , PointType::sunssf }, - {"W" , PointType::int16}, - {"W_SF" , PointType::sunssf}, - {"MA1" , PointType::string, 8 }, - {"RCnt" , PointType::uint32}, - {"OS" , PointType::uint32}, - {"Epoch" , PointType::uint32}, - {"TZO" , PointType::int16 }, - {"EpochSetCnt" , PointType::uint32 }, - {"EpochSetOS" , PointType::uint32 }, - {"DI" , PointType::uint16 }, - {"DO" , PointType::uint16 }, - {"Meta1" , PointType::string, 70 }, - {"Meta2" , PointType::string, 50 }, - {"Meta3" , PointType::string, 50 }, - {"Evt" , PointType::bitfield32 }, - {"NSig" , PointType::uint16 }, - {"BSig" , PointType::uint16 }, - {"Sig" , PointType::blob }, - -}; -// clang-format on - -enum struct SnapshotType { - CURRENT = 0, - TURN_ON = 1, - TURN_OFF = 2, - START = 3, - END = 4 -}; - -enum SnapshotStatus { - VALID = 0, - INVALID = 1, - UPDATING = 2, - FAILED_GENERAL = 3, - FAILED_NOT_ENABLED = 4, - FAILED_FEEDBACK = 5 -}; - -// BSM custom model 64901 BSM style singed snapshot -class SignedSnapshot : public SunspecModelBase { - -public: - explicit SignedSnapshot(const transport::DataVector& data) : SunspecModelBase(data) { - } - - // {"Type" , PointType::enum16 }, - sunspec::enum16 Type() const { - return enum16_at(Model[0].offset); - } - - // {"Status" , PointType::enum16 }, - sunspec::enum16 Status() const { - return enum16_at(Model[1].offset); - } - - // {"RCR" , PointType::acc32 }, - sunspec::acc32 RCR() const { - return acc32_at(Model[2].offset); - } - - // {"TotWhImp" , PointType::acc32 }, - sunspec::acc32 TotWhImp() const { - return acc32_at(Model[3].offset); - } - - // {"Wh_SF" , PointType::sunssf }, - sunspec::sunssf Wh_SF() const { - return sunssf_at(Model[4].offset); - } - - // {"W" , PointType::int16}, - sunspec::int16 W() const { - return int16_at(Model[5].offset); - } - - // {"W_SF" , PointType::sunssf}, - sunspec::sunssf W_SF() const { - return sunssf_at(Model[6].offset); - } - - // {"MA1" , PointType::string, 8 }, - sunspec::string MA1() const { - return string_at_with_length(Model[7].offset, Model[7].length_in_bytes); - } - - // {"RCnt" , PointType::uint32}, - sunspec::uint32 RCnt() const { - return uint32_at(Model[8].offset); - } - - // {"OS" , PointType::uint32}, - sunspec::uint32 OS() const { - return uint32_at(Model[9].offset); - } - - // {"Epoch" , PointType::uint32}, - sunspec::uint32 Epoch() const { - return uint32_at(Model[10].offset); - } - - // {"TZO" , PointType::int16 }, - sunspec::int16 TZO() const { - return int16_at(Model[11].offset); - } - - // {"EpochSetCnt" , PointType::uint32 }, - sunspec::uint32 EpochSetCnt() const { - return uint32_at(Model[12].offset); - } - - // {"EpochSetOS" , PointType::uint32 }, - sunspec::uint32 EpochSetOS() const { - return uint32_at(Model[13].offset); - } - - // {"DI" , PointType::uint16 }, - sunspec::uint16 DI() const { - return uint16_at(Model[14].offset); - } - - // {"DO" , PointType::uint16 }, - sunspec::uint16 DO() const { - return uint16_at(Model[15].offset); - } - - // {"Meta1" , PointType::string, 70 }, - sunspec::string Meta1() const { - return string_at_with_length(Model[16].offset, Model[16].length_in_bytes); - } - - // {"Meta2" , PointType::string, 50 }, - sunspec::string Meta2() const { - return string_at_with_length(Model[17].offset, Model[17].length_in_bytes); - } - - // {"Meta3" , PointType::string, 50 }, - sunspec::string Meta3() const { - return string_at_with_length(Model[18].offset, Model[18].length_in_bytes); - } - - // {"Evt" , PointType::bitfield32 } - sunspec::bitfield32 Evt() const { - return bitfield32_at(Model[19].offset); - } - - // {"NSig" , PointType::uint16 }, - sunspec::uint16 NSig() const { - return uint16_at(Model[20].offset); - } - - // {"BSig" , PointType::uint16 }, - sunspec::uint16 BSig() const { - return uint16_at(Model[21].offset); - } - - sunspec::string Sig() const { - return string_at_with_length_from_binary_data(Model[22].offset, BSig()); - } - - sunspec::string SigPadded() const { - return string_at_with_length_from_binary_data(Model[22].offset, NSig() * 2); - } -}; - -// BSM custom model 64903 OCMF signed snapshot -// 41792 1 ID Model ID uint16 no 64903 Model OCMF data -// 41793 1 L Model Payload Length uint16 no 372 Without the fields 'Model ID' and 'Length of payload. -// 41794 1 Typ Snapshot Type enum16 no 0 Signed snapshot status, see chapter 17.5 -// 41795 1 St Snapshot Status enum16 no See chapter 17.5, write to the Status field of the corresponding snapshot to -// create it. 41796 496 O OCMF string no OCMF representation of the snapshot “Signed Current Snapshot”, the metadata -// field 1 is used as OCMF identity - -// clang-format off -inline constexpr PointInitializerList BSM_OCMFSnapshotPointInitList = { - - {"ID", PointType::uint16}, - {"L", PointType::uint16}, - {"Type", PointType::enum16}, - {"St", PointType::enum16}, - {"O", PointType::string, 496} - -}; -// clang-format on - -class SignedOCMFSnapshot - : public SunspecModelBase { - -public: - explicit SignedOCMFSnapshot(const transport::DataVector& data) : SunspecModelBase(data) { - } - - // { "ID", PointType::uint16 }, - sunspec::uint16 ID() const { - constexpr std::size_t index = 0; - return uint16_at(Model[index].offset); - } - - // { "L", PointType::uint16 }, - sunspec::uint16 L() const { - constexpr std::size_t index = 1; - return uint16_at(Model[index].offset); - } - - // { "Type", PointType::enum16 }, - sunspec::enum16 Type() const { - constexpr std::size_t index = 2; - return enum16_at(Model[index].offset); - } - - // { "St", PointType::enum16 }, - sunspec::enum16 St() const { - constexpr std::size_t index = 3; - return enum16_at(Model[index].offset); - } - - // { "O", PointType::string , 496 } - sunspec::string O() const { - constexpr std::size_t index = 4; - return string_at_with_length(Model[index].offset, Model[index].length_in_bytes); - } -}; -} // namespace bsm - -#endif // POWERMETER_BSM_BSMSNAPSHOTMODEL_HPP diff --git a/modules/PowermeterBSM/lib/known_model.cpp b/modules/PowermeterBSM/lib/known_model.cpp deleted file mode 100644 index d8ef922ec..000000000 --- a/modules/PowermeterBSM/lib/known_model.cpp +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#include "known_model.hpp" - -namespace known_model { - -/* - * Warning: only specify *addresses* relative to the sunspec base address, - * the transport layer adds the relative addresses to the sunspec base address when querying the device. - */ - -const AddressData Sunspec_Common{3_sma, calc_model_size_in_register(sunspec_model::Common::Model)}; -const AddressData Sunspec_ACMeter{91_sma, calc_model_size_in_register(sunspec_model::ACMeter::Model)}; - -// This starts reading two registers after the "official beginning of the model, we dont need model id and payload -// length. -const AddressData BSM_CurrentSnapshot{524_sma, calc_model_size_in_register(bsm::SignedSnapshot::Model)}; -// the turn on/turn off snapshots go here - -const AddressData BSM_OCMF_CurrentSnapshot{1792_sma, calc_model_size_in_register(bsm::SignedOCMFSnapshot::Model)}; -// the turn on/turn off snapshots go here - -} // namespace known_model diff --git a/modules/PowermeterBSM/lib/known_model.hpp b/modules/PowermeterBSM/lib/known_model.hpp deleted file mode 100644 index 2fa93b120..000000000 --- a/modules/PowermeterBSM/lib/known_model.hpp +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_KNOWN_MODEL_HPP -#define POWERMETER_BSM_KNOWN_MODEL_HPP - -#include "BSMSnapshotModel.hpp" -#include "sunspec_models.hpp" // sunspec models - -/** - * The namespace known_model contains predefined, widly known sunpec data models. - * This includes standard models (prefixed with "Sunspec_") and custom models, - * (prefixed with something vendor related). - */ -namespace known_model { - -// this describes where to get the model data: the offset to the sunspec base address and the model length. -struct AddressData { - const protocol_related_types::SunspecDataModelAddress base_offset; - const std::size_t model_size; -}; - -extern const AddressData Sunspec_Common; -extern const AddressData Sunspec_ACMeter; -extern const AddressData BSM_CurrentSnapshot; -extern const AddressData BSM_OCMF_CurrentSnapshot; - -} // namespace known_model - -#endif // POWERMETER_BSM_KNOWN_MODEL_HPP diff --git a/modules/PowermeterBSM/lib/protocol_related_types.cpp b/modules/PowermeterBSM/lib/protocol_related_types.cpp deleted file mode 100644 index cc464a463..000000000 --- a/modules/PowermeterBSM/lib/protocol_related_types.cpp +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#include "protocol_related_types.hpp" - -namespace protocol_related_types { - -ModbusRegisterAddress ModbusRegisterAddress::operator=(const SunspecDataModelAddress& sma) { - val = sma.val - 1; - return *this; -} -SunspecDataModelAddress::SunspecDataModelAddress(const ModbusRegisterAddress& mra) : val(mra.val + 1) { -} - -SunspecDataModelAddress& SunspecDataModelAddress::operator=(const ModbusRegisterAddress& mra) { - val = mra.val + 1; - return *this; -} - -} // namespace protocol_related_types - -protocol_related_types::SunspecDataModelAddress operator"" _sma(unsigned long long int v) { - - return protocol_related_types::SunspecDataModelAddress{static_cast(v)}; -} - -protocol_related_types::ModbusRegisterAddress operator"" _mra(unsigned long long int v) { - - return protocol_related_types::ModbusRegisterAddress{static_cast(v)}; -} - -protocol_related_types::SunspecDataModelAddress operator+(const protocol_related_types::SunspecDataModelAddress& s0, - const protocol_related_types::SunspecDataModelAddress& s1) { - - return protocol_related_types::SunspecDataModelAddress(s0.val + s1.val); -} - -protocol_related_types::ModbusRegisterAddress operator+(const protocol_related_types::ModbusRegisterAddress& m0, - const protocol_related_types::ModbusRegisterAddress& m1) { - - return protocol_related_types::ModbusRegisterAddress(m0.val + m1.val); -} - -protocol_related_types::ModbusRegisterAddress operator+(const protocol_related_types::ModbusRegisterAddress& m0, - const protocol_related_types::SunspecDataModelAddress& s1) { - - return protocol_related_types::ModbusRegisterAddress(m0.val + s1.val - 1); -} diff --git a/modules/PowermeterBSM/lib/protocol_related_types.hpp b/modules/PowermeterBSM/lib/protocol_related_types.hpp deleted file mode 100644 index 66b3d819f..000000000 --- a/modules/PowermeterBSM/lib/protocol_related_types.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_BASE_TYPES_HPP -#define POWERMETER_BSM_BASE_TYPES_HPP - -#include -#include - -namespace transport { -using DataVector = std::vector; -} - -namespace protocol_related_types { - -using SunspecDataModelOffset = std::uint16_t; -using SunspecRegisterCount = std::uint16_t; -using SunspecModelId = std::uint16_t; -using ModbusUnitId = std::uint16_t; - -struct ModbusRegisterAddress; - -struct SunspecDataModelAddress { - - std::uint16_t val; - - explicit SunspecDataModelAddress(const ModbusRegisterAddress&); - - explicit SunspecDataModelAddress(std::uint16_t v) : val(v) { - } - - SunspecDataModelAddress& operator=(const ModbusRegisterAddress&); -}; - -struct ModbusRegisterAddress { - - std::uint16_t val; - - explicit ModbusRegisterAddress(const SunspecDataModelAddress& sma) : val(sma.val - 1) { - } - - explicit ModbusRegisterAddress(std::uint16_t v) : val(v) { - } - - ModbusRegisterAddress operator=(const SunspecDataModelAddress& sma); -}; - -} // namespace protocol_related_types - -protocol_related_types::SunspecDataModelAddress operator"" _sma(unsigned long long int v); -protocol_related_types::ModbusRegisterAddress operator"" _mra(unsigned long long int v); - -protocol_related_types::SunspecDataModelAddress operator+(const protocol_related_types::SunspecDataModelAddress& s0, - const protocol_related_types::SunspecDataModelAddress& s1); -protocol_related_types::ModbusRegisterAddress operator+(const protocol_related_types::ModbusRegisterAddress& m0, - const protocol_related_types::ModbusRegisterAddress& m1); -protocol_related_types::ModbusRegisterAddress operator+(const protocol_related_types::ModbusRegisterAddress& m0, - const protocol_related_types::SunspecDataModelAddress& s1); - -#endif // POWERMETER_BSM_BASE_TYPES_HPP diff --git a/modules/PowermeterBSM/lib/sunspec_base.hpp b/modules/PowermeterBSM/lib/sunspec_base.hpp deleted file mode 100644 index f4f942e2a..000000000 --- a/modules/PowermeterBSM/lib/sunspec_base.hpp +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_SUNSPEC_BASE_HPP -#define POWERMETER_BSM_SUNSPEC_BASE_HPP - -// see ./sunspec_data_types.text for further information about sunspec types. - -#include "protocol_related_types.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std::string_literals; - -enum struct PointType { - acc32, - bitfield32, - enum16, - int16, - pad, - sunssf, - uint16, - uint32, - string, - blob -}; - -namespace invalid_point_value { - -const std::uint32_t acc32{0}; -const std::uint32_t bitfield32{0x80000000}; -const std::uint16_t enum16{0xffff}; -const std::int16_t int16{static_cast(0x8000)}; -const std::uint16_t pad{0x8000}; -const std::uint16_t sunssf{0x8000}; -const std::uint16_t uint16{0xffff}; -const std::uint32_t uint32{0xffffffff}; - -inline bool valid_string(std::string s) { - return not(s.empty() || s.at(0) == 0); // string starts with a endmarker -} - -}; // namespace invalid_point_value - -// clang-format off -const std::map point_to_string_map { - - { PointType::acc32 , "acc32" }, - { PointType::bitfield32 , "bitfield32" }, - { PointType::enum16 , "enum16" }, - { PointType::int16 , "int16" }, - { PointType::pad , "pad" }, - { PointType::sunssf , "sunssf" }, - { PointType::uint16 , "uint16" }, - { PointType::uint32 , "uint32" }, - { PointType::string , "string" }, - { PointType::blob , "blob" }, - -}; -// clang-format on - -static std::string point_type_to_string(PointType pointType) { - - auto it = point_to_string_map.find(pointType); - - return it == point_to_string_map.end() ? "unknown type" : it->second; -} - -constexpr std::uint16_t point_length_in_bytes(PointType pointType) { - - switch (pointType) { - case PointType::acc32: - case PointType::bitfield32: - case PointType::uint32: - return 4; - - case PointType::enum16: - case PointType::int16: - case PointType::sunssf: - case PointType::uint16: - case PointType::blob: // length information must be obained from model data - return 2; - - case PointType::string: // this is a bit hackish... a point_length_in_bytes means that the initial point list has to - // provide a length value (see calc_offset for this). - case PointType::pad: - return 0; - } -} - -struct Point { - const char* id; - PointType pointType; - std::uint16_t length_in_bytes = 0; - std::uint16_t offset = 0; - - std::string to_string() const { - std::stringstream ss; - ss << "id: [" << std::setw(12) << id << " ]" - << " type: [" << std::setw(12) << point_type_to_string(pointType) << " ]" - << " length: [" << std::setw(3) << length_in_bytes << " ]" - << " offset: [" << std::setw(3) << offset << " ]"; - return ss.str(); - } -}; - -namespace sunspec_decoder { - -inline std::uint16_t uint16_at(const transport::DataVector& data, transport::DataVector::size_type offset) { - return be16toh((*reinterpret_cast(std::addressof(data.data()[offset])))); -} - -inline std::int16_t int16_at(const transport::DataVector& data, transport::DataVector::size_type offset) { - return be16toh((*reinterpret_cast(std::addressof(data.data()[offset])))); -} - -inline std::uint32_t uint32_at(const transport::DataVector& data, transport::DataVector::size_type offset) { - return be32toh((*reinterpret_cast(std::addressof(data.data()[offset])))); -} - -inline std::string string_at_with_length(const transport::DataVector& data, transport::DataVector::size_type offset, - transport::DataVector::size_type length) { - // if dirty, then be completely dirty.. - return std::string(reinterpret_cast(std::addressof(data.data()[offset])), length).c_str(); -} - -inline std::string string_at_with_length_from_binary_data(const transport::DataVector& data, - transport::DataVector::size_type offset, - transport::DataVector::size_type length) { - - std::stringstream ss; - - for (std::size_t index = 0; index < length; ++index) - ss << std::hex << std::setfill('0') << std::setw(2) << int(data[index + offset]); - - return ss.str(); -} - -} // namespace sunspec_decoder - -inline std::string point_value_to_string(const transport::DataVector& data, const Point& point) { - - switch (point.pointType) { - case PointType::acc32: - case PointType::bitfield32: - case PointType::uint32: - return std::to_string(sunspec_decoder::uint32_at(data, point.offset)); - case PointType::enum16: - case PointType::pad: - case PointType::sunssf: - case PointType::uint16: - return std::to_string(sunspec_decoder::uint16_at(data, point.offset)); - case PointType::int16: - return std::to_string(sunspec_decoder::int16_at(data, point.offset)); - case PointType::string: - return sunspec_decoder::string_at_with_length(data, point.offset, point.length_in_bytes); - case PointType::blob: - return std::string("not yet implemented!"); - } - - return std::string("Unknown point type!"); -} - -using PointInitializerList = std::initializer_list; - -template using PointArray = std::array; - -template constexpr std::size_t calc_model_size_in_bytes(const PointArray& model_data) { - - std::size_t model_length = 0; - for (const auto& point : model_data) - model_length += point.length_in_bytes; - - return model_length; -} - -template constexpr std::size_t calc_model_size_in_register(const PointArray& model_data) { - - return calc_model_size_in_bytes(model_data) / 2; -} - -template constexpr PointArray calc_offset(const PointInitializerList& initList) { - - PointArray result{}; - std::uint16_t offset = 0; - auto resultIterator = result.begin(); - for (auto initItem : initList) { - *resultIterator = initItem; - auto length = point_length_in_bytes(initItem.pointType); - (*resultIterator).length_in_bytes = length == 0 - ? initItem.length_in_bytes * 2 - : // this is string / pad length in registers * 2 --> length in bytes - length; - if ((*resultIterator).length_in_bytes == 0) - throw std::logic_error("init list length error."); - (*resultIterator).offset = offset; - offset += (*resultIterator).length_in_bytes; - resultIterator++; - } - return result; -} - -// clang-format off -namespace sunspec { - -using acc32 = std::uint32_t; -using bitfield32 = std::uint32_t; -using enum16 = std::uint16_t; -using int16 = std::int16_t; -using pad = std::int16_t; -using sunssf = std::int16_t; -using uint16 = std::uint16_t; -using uint32 = std::uint32_t; -using string = std::string; - -} -// clang-format on - -template struct SunspecModelBase { - - static constexpr PointArray Model{calc_offset(PI)}; - - const std::size_t model_size_in_bytes{calc_model_size_in_bytes(Model)}; - const std::size_t model_size_in_register{calc_model_size_in_register(Model)}; - - const transport::DataVector m_data; - - std::uint16_t uint16_at(transport::DataVector::size_type offset) const { - return sunspec_decoder::uint16_at(m_data, offset); - } - - std::int16_t int16_at(transport::DataVector::size_type offset) const { - return sunspec_decoder::int16_at(m_data, offset); - } - - std::uint32_t uint32_at(transport::DataVector::size_type offset) const { - return sunspec_decoder::uint32_at(m_data, offset); - } - - std::string string_at_with_length(transport::DataVector::size_type offset, - transport::DataVector::size_type length) const { - return sunspec_decoder::string_at_with_length(m_data, offset, length); - } - - std::string string_at_with_length_from_binary_data(transport::DataVector::size_type offset, - transport::DataVector::size_type length) const { - return sunspec_decoder::string_at_with_length_from_binary_data(m_data, offset, length); - } - - sunspec::acc32 acc32_at(transport::DataVector::size_type offset) const { - return uint32_at(offset); - } - sunspec::bitfield32 bitfield32_at(transport::DataVector::size_type offset) const { - return uint32_at(offset); - } - - sunspec::enum16 enum16_at(transport::DataVector::size_type offset) const { - return uint16_at(offset); - } - sunspec::pad pad_at(transport::DataVector::size_type offset) const { - return uint16_at(offset); - } - sunspec::sunssf sunssf_at(transport::DataVector::size_type offset) const { - return uint16_at(offset); - } - - SunspecModelBase() = delete; - - explicit SunspecModelBase(const transport::DataVector& data) : m_data(data) { - if (data.size() < model_size_in_bytes) - throw std::runtime_error(""s + __PRETTY_FUNCTION__ + " Data container size (" + - std::to_string(data.size()) + ") is smaller than model size (" + - std::to_string(model_size_in_bytes) + ") !"); - } -}; - -template -std::ostream& model_to_stream(std::ostream& out, const MODEL& model, std::initializer_list index_list) { - - for (std::size_t model_index : index_list) { - out << "Id : " << MODEL::Model[model_index].id << " offset " << MODEL::Model[model_index].offset << " " - << " value " << point_value_to_string(model.m_data, MODEL::Model[model_index]) << "\n"; - } - - return out; -} - -#endif // POWERMETER_BSM_SUNSPEC_BASE_HPP diff --git a/modules/PowermeterBSM/lib/sunspec_data_types.text b/modules/PowermeterBSM/lib/sunspec_data_types.text deleted file mode 100644 index 1ee05ba69..000000000 --- a/modules/PowermeterBSM/lib/sunspec_data_types.text +++ /dev/null @@ -1,11 +0,0 @@ - sunspec data types: - - Type Description Forming the value from register contents Value range Not available or invalid - acc32 32 bit meter, unsigned like uint32 0 - 4294967295 0 - bitfield32 Collection of 15 bit information like uint32 0 - 0x7fff If bit 32 is set (0x80000000) - enum16 16 bit enumeration like uint16 0 - 65534 65535 (0xffff) - int16 16 bit integer, signed (int16_t)R[n] -32767 ... 32767 -32768 (0x8000) - pad Filling data like int16 0x8000 -32768 (0x8000) - sunsf Scaling factor (int16_t)R[n] -10 to 10 -32768 (0x8000) - uint16 16 bit integer, unsigned (uint16_t)R[n] 0 to 65534 65535 (0xffff) - uint32 32 bit integer, unsigned (uint32_t)R[n] << 16 | (uint32_t)R[n + 1] 0 to 4294967294 4294967295 diff --git a/modules/PowermeterBSM/lib/sunspec_models.hpp b/modules/PowermeterBSM/lib/sunspec_models.hpp deleted file mode 100644 index 211e27d53..000000000 --- a/modules/PowermeterBSM/lib/sunspec_models.hpp +++ /dev/null @@ -1,329 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_SUNSPEC_MODELS_HPP -#define POWERMETER_BSM_SUNSPEC_MODELS_HPP - -#include "sunspec_base.hpp" - -////////////////////////////////////////////////////////////////////// -// -// sunspec models - -namespace sunspec_model { - -// clang-format off -inline constexpr PointInitializerList CommonModelInitList = { - - {"ID" , PointType::uint16}, - {"L" , PointType::uint16}, - {"Mn" , PointType::string, 16}, - {"Md" , PointType::string, 16}, - {"Opt" , PointType::string, 8}, - {"Vr" , PointType::string, 8}, - {"SN" , PointType::string, 16}, - {"DA" , PointType::uint16}, - {"Pad" , PointType::pad, 1} - -}; -// clang-format on - -class Common : public SunspecModelBase { - -public: - explicit Common(const transport::DataVector& data) : SunspecModelBase(data) { - } - - sunspec::string Mn() const { - return string_at_with_length(Model[2].offset, Model[2].length_in_bytes); - } - sunspec::string Md() const { - return string_at_with_length(Model[3].offset, Model[3].length_in_bytes); - } - sunspec::string Vr() const { - return string_at_with_length(Model[5].offset, Model[5].length_in_bytes); - } - sunspec::uint16 Da() const { - return uint16_at(Model[8].offset); - } -}; - -////////////////////////////////////////////////////////////////////// -// -// sunspec AC meter - -// clang-format off -inline constexpr PointInitializerList ACMeterInitList = { - - { "ID" , PointType::uint16 } , // 40091 1 ID Model ID uint16 no 203 SunSpec Model AC Meter - { "L" , PointType::uint16 } , // 40092 1 L Model Payload Length uint16 no 105 Without the fields 'Model ID' and 'Length of payload. - { "A" , PointType::int16 } , // 40093 1 A Amps int16 A no - { "AphA" , PointType::int16 } , // 40094 1 AphA Amps PhaseA int16 A no - { "AphB" , PointType::int16 } , // 40095 1 AphB Amps PhaseB int16 A no - { "AphC" , PointType::int16 } , // 40096 1 AphC Amps PhaseC int16 A no - { "A_SF" , PointType::int16 } , // 40097 1 A_SF sunssf no - { "no" , PointType::pad , 1 } , // 40098 1 no Reserved - { "PhVphA" , PointType::int16 } , // 40099 1 PhVphA Phase Voltage AN int16 V no - { "PhVphB" , PointType::int16 } , // 40100 1 PhVphB Phase Voltage BN int16 V no - { "PhVphC" , PointType::int16 } , // 40101 1 PhVphC Phase Voltage CN int16 V no - { "no" , PointType::pad , 4 } , // 40102 4 no Reserved - { "V_SF" , PointType::sunssf } , // 40106 1 V_SF sunssf no - { "Hz" , PointType::int16 } , // 40107 1 Hz Hz int16 Hz no - { "Hz_SF" , PointType::sunssf } , // 40108 1 Hz_SF sunssf no - { "W" , PointType::int16 } , // 40109 1 W Watts int16 W no - { "WphA" , PointType::int16 } , // 40110 1 WphA Watts phase A int16 W no - { "WphB" , PointType::int16 } , // 40111 1 WphB Watts phase B int16 W no - { "WphC" , PointType::int16 } , // 40112 1 WphC Watts phase C int16 W no - { "W_SF" , PointType::sunssf } , // 40113 1 W_SF sunssf no - { "VA" , PointType::int16 } , // 40114 1 VA VA int16 VA no - { "VAphA" , PointType::int16 } , // 40115 1 VAphA VA phase A int16 VA no - { "VAphB" , PointType::int16} , // 40116 1 VAphB VA phase B int16 VA no - { "VAphC" , PointType::int16 } , // 40117 1 VAphC VA phase C int16 VA no - { "VA_SF" , PointType::int16 } , // 40118 1 VA_SF sunssf no - { "VAR" , PointType::int16 } , // 40119 1 VAR VAR int16 var no - { "VARphA" , PointType::int16 } , // 40120 1 VARphA VAR phase A int16 var no - { "VARphB" , PointType::int16 } , // 40121 1 VARphB VAR phase B int16 var no - { "VARphC" , PointType::int16 } , // 40122 1 VARphC VAR phase C int16 var no - { "VAR_SF" , PointType::sunssf } , // 40123 1 VAR_SF sunssf no - { "no" , PointType::pad , 1 } , // 40124 1 no Reserved - { "PFphA" , PointType::int16 } , // 40125 1 PFphA PF phase A int16 Pct no - { "PFphB" , PointType::int16 } , // 40126 1 PFphB PF phase B int16 Pct no - { "PFphC" , PointType::int16 } , // 40127 1 PFphC PF phase C int16 Pct no - { "PF_SF" , PointType::sunssf } , // 40128 1 PF_SF sunssf no - { "no" , PointType::pad , 8 } , // 40129 8 no Reserved - { "TotWhIm" , PointType::acc32 } , // 40137 2 TotWhIm p Total Watt-hours Imported acc32 Wh no - { "no" , PointType::pad , 6 } , // 40139 6 no Reserved - { "TotWh_SF" , PointType::sunssf } , // 40145 1 TotWh_SF sunssf no - { "no" , PointType::string, 50 } , // 40146 50 no Reserved - { "Evt" , PointType::bitfield32 } , // 40196 2 Evt Events bitfield32 no See chapter 17.5 Event flags of critical events of counter and communication module. A problem exists if this value is different from zero. -}; -// clang-format on - -class ACMeter : public SunspecModelBase { - -public: - explicit ACMeter(const transport::DataVector& data) : SunspecModelBase(data) { - } - - // { "ID" , PointType::uint16 } , // 40091 1 ID Model ID uint16 no 203 SunSpec Model AC Meter - sunspec::uint16 ID() const { - std::size_t index = 0; - return uint16_at(Model.at(index).offset); - } - - // { "L" , PointType::uint16 } , // 40092 1 L Model Payload Length uint16 no 105 Without the fields - // 'Model ID' and 'Length of payload. - sunspec::uint16 L() const { - constexpr std::size_t index = 1; - return uint16_at(Model.at(index).offset); - } - - // { "A" , PointType::int16 } , // 40093 1 A Amps int16 A no - sunspec::int16 A() const { - constexpr std::size_t index = 2; - return int16_at(Model.at(index).offset); - } - - // { "AphA" , PointType::int16 } , // 40094 1 AphA Amps PhaseA int16 A no - sunspec::int16 AphA() const { - constexpr std::size_t index = 3; - return (Model.at(index).offset); - } - - // { "AphB" , PointType::int16 } , // 40096 1 AphB Amps PhaseB int16 B no - sunspec::int16 AphB() const { - constexpr std::size_t index = 4; - return int16_at(Model.at(index).offset); - } - - // { "AphC" , PointType::int16 } , // 40096 1 AphC Amps PhaseC int16 A no - sunspec::int16 AphC() const { - constexpr std::size_t index = 5; - return int16_at(Model.at(index).offset); - } - - // { "A_SF" , PointType::int16 } , // 40097 1 A_SF sunssf no - sunspec::int16 A_SF() const { - constexpr std::size_t index = 6; - return int16_at(Model.at(index).offset); - } - - // { "no" , PointType::pad } , // 40098 1 no Reserved - // { "PhVphA" , PointType::int16 } , // 40099 1 PhVphA Phase Voltage AN int16 V no - sunspec::int16 PhVphA() const { - const std::size_t index = 8; - return int16_at(Model.at(index).offset); - } - - // { "PhVphB" , PointType::int16 } , // 40100 1 PhVphB Phase Voltage BN int16 V no - sunspec::int16 PhVphB() const { - constexpr std::size_t index = 9; - return int16_at(Model.at(index).offset); - } - - // { "PhVphC" , PointType::int16 } , // 40101 1 PhVphC Phase Voltage CN int16 V no - sunspec::int16 PhVphC() const { - constexpr std::size_t index = 10; - return int16_at(Model.at(index).offset); - } - - // { "no" , PointType::pad } , // 40102 4 no Reserved - // { "V_SF" , PointType::sunssf } , // 40106 1 V_SF sunssf no - sunspec::sunssf V_SF() const { - constexpr std::size_t index = 12; - return sunssf_at(Model.at(index).offset); - } - - // { "Hz" , PointType::int16 } , // 40107 1 Hz Hz int16 Hz no - sunspec::int16 Hz() const { - constexpr std::size_t index = 13; - return int16_at(Model.at(index).offset); - } - - // { "Hz_SF" , PointType::sunssf } , // 40108 1 Hz_SF sunssf no - sunspec::sunssf Hz_SF() const { - constexpr std::size_t index = 14; - return sunssf_at(Model.at(index).offset); - } - - // { "W" , PointType::int16 } , // 40109 1 W Watts int16 W no - sunspec::int16 W() const { - constexpr std::size_t index = 15; - return int16_at(Model.at(index).offset); - } - - // { "WphA" , PointType::int16 } , // 40110 1 WphA Watts phase A int16 W no - sunspec::int16 WphA() const { - constexpr std::size_t index = 16; - return int16_at(Model.at(index).offset); - } - - // { "WphB" , PointType::int16 } , // 40111 1 WphB Watts phase B int16 W no - sunspec::int16 WphB() const { - constexpr std::size_t index = 17; - return int16_at(Model.at(index).offset); - } - - // { "WphC" , PointType::int16 } , // 40112 1 WphC Watts phase C int16 W no - sunspec::int16 WphC() const { - constexpr std::size_t index = 18; - return int16_at(Model.at(index).offset); - } - - // { "W_SF" , PointType::sunssf } , // 40113 1 W_SF sunssf no - sunspec::sunssf W_SF() const { - constexpr std::size_t index = 19; - return sunssf_at(Model.at(index).offset); - } - - // { "VA" , PointType::int16 } , // 40114 1 VA VA int16 VA no - sunspec::int16 VA() const { - constexpr std::size_t index = 20; - return int16_at(Model.at(index).offset); - } - - // { "VAphA" , PointType::int16 } , // 40115 1 VAphA VA phase A int16 VA no - sunspec::int16 VAphA() const { - constexpr std::size_t index = 21; - return int16_at(Model.at(index).offset); - } - - // { "VAphB" , PointType::int16} , // 40116 1 VAphB VA phase B int16 VA no - sunspec::int16 VAphB() const { - constexpr std::size_t index = 22; - return int16_at(Model.at(index).offset); - } - - // { "VAphC" , PointType::int16 } , // 40117 1 VAphC VA phase C int16 VA no - sunspec::int16 VAphC() const { - constexpr std::size_t index = 23; - return int16_at(Model.at(index).offset); - } - - // { "VA_SF" , PointType::int16 } , // 40118 1 VA_SF sunssf no - sunspec::int16 VA_SF() const { - constexpr std::size_t index = 24; - return int16_at(Model.at(index).offset); - } - - // { "VAR" , PointType::int16 } , // 40119 1 VAR VAR int16 var no - sunspec::int16 VAR() const { - constexpr std::size_t index = 25; - return int16_at(Model.at(index).offset); - } - - // { "VARphA" , PointType::int16 } , // 40120 1 VARphA VAR phase A int16 var no - sunspec::int16 VARphA() const { - constexpr std::size_t index = 26; - return int16_at(Model.at(index).offset); - } - - // { "VARphB" , PointType::int16 } , // 40121 1 VARphB VAR phase B int16 var no - sunspec::int16 VARphB() const { - constexpr std::size_t index = 27; - return int16_at(Model.at(index).offset); - } - - // { "VARphC" , PointType::int16 } , // 40122 1 VARphC VAR phase C int16 var no - sunspec::int16 VARphC() const { - constexpr std::size_t index = 28; - return int16_at(Model.at(index).offset); - } - - // { "VAR_SF" , PointType::sunssf } , // 40123 1 VAR_SF sunssf no - sunspec::sunssf VAR_SF() const { - constexpr std::size_t index = 29; - return sunssf_at(Model.at(index).offset); - } - - // { "no" , PointType::pad} , // 40124 1 no Reserved - // { "PFphA" , PointType::int16 } , // 40125 1 PFphA PF phase A int16 Pct no - sunspec::int16 PFphA() const { - constexpr std::size_t index = 31; - return int16_at(Model.at(index).offset); - } - - // { "PFphB" , PointType::int16 } , // 40126 1 PFphB PF phase B int16 Pct no - sunspec::int16 PFphB() const { - constexpr std::size_t index = 32; - return int16_at(Model.at(index).offset); - } - - // { "PFphC" , PointType::int16 } , // 40127 1 PFphC PF phase C int16 Pct no - sunspec::int16 PFphC() const { - constexpr std::size_t index = 33; - return int16_at(Model.at(index).offset); - } - - // { "PF_SF" , PointType::sunssf } , // 40128 1 PF_SF sunssf no - sunspec::sunssf PF_SF() const { - constexpr std::size_t index = 34; - return sunssf_at(Model.at(index).offset); - } - - // { "no" , PointType::pad } , // 40129 8 no Reserved - // { "TotWhIm" , PointType::acc32 } , // 40137 2 TotWhIm p Total Watt-hours Imported acc32 Wh no - sunspec::acc32 TotWhIm() const { - constexpr std::size_t index = 36; - return acc32_at(Model.at(index).offset); - } - - // { "no" , PointType::pad } , // 40139 6 no Reserved - // { "TotWh_SF" , PointType::sunssf } , // 40145 1 TotWh_SF sunssf no - sunspec::sunssf TotWh_SF() const { - constexpr std::size_t index = 38; - return sunssf_at(Model.at(index).offset); - } - - // { "no" , PointType::pad, 50 } , // 40146 50 no Reserved - // { "Evt" , PointType::bitfield32 } , // 40196 2 Evt Events bitfield32 no See chapter 17.5 Event flags of - // critical events of counter and communication module. A problem exists if this value is different from zero. - sunspec::bitfield32 Evt() const { - constexpr std::size_t index = 40; - return bitfield32_at(Model.at(index).offset); - } -}; - -} // namespace sunspec_model - -#endif // POWERMETER_BSM_SUNSPEC_MODELS_HPP diff --git a/modules/PowermeterBSM/lib/transport.cpp b/modules/PowermeterBSM/lib/transport.cpp deleted file mode 100644 index b908deb2f..000000000 --- a/modules/PowermeterBSM/lib/transport.cpp +++ /dev/null @@ -1,195 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#include "transport.hpp" - -#include -#include -#include -#include - -namespace transport { - -ModbusTransport::ModbusTransport(std::string serial_device_name, protocol_related_types::ModbusUnitId unit_id, - protocol_related_types::ModbusRegisterAddress base_address) : - m_cfg(serial_device_name), - m_device(m_cfg), - m_connection(m_device), - m_modbus_client(m_connection), - m_unit_id(unit_id), - m_base_address(base_address) { -} - -bool ModbusTransport::trigger_snapshot_generation( - protocol_related_types::ModbusRegisterAddress snapshot_trigger_register) { - - using namespace std::chrono_literals; - - const everest::modbus::DataVectorUint16 bsm_powermeter_create_snapshot_command{0x0002}; - everest::modbus::ModbusDataContainerUint16 outgoing(everest::modbus::ByteOrder::LittleEndian, - bsm_powermeter_create_snapshot_command); - - m_modbus_client.write_multiple_registers(m_unit_id, snapshot_trigger_register.val, 1, outgoing, false); - - std::uint16_t response_status{}; - - std::size_t counter = 10; - - while (counter-- > 0) { - - transport::DataVector response = - m_modbus_client.read_holding_register(m_unit_id, snapshot_trigger_register.val, true); - - response_status = be16toh((*reinterpret_cast(&response.data()[0]))); - - if (response_status == 0) - return true; - - std::this_thread::sleep_for(750ms); - } - - return false; -} - -bool ModbusTransport::trigger_snapshot_generation_BSM() { - - return trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress(40525_sma)); -} - -bool ModbusTransport::trigger_snapshot_generation_BSM_OCMF() { - - return trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress(41795_sma)); -} - -transport::DataVector ModbusTransport::fetch(const known_model::AddressData& ad) { - return fetch(ad.base_offset, ad.model_size); -} - -transport::DataVector ModbusTransport::fetch(protocol_related_types::SunspecDataModelAddress model_address, - protocol_related_types::SunspecRegisterCount model_length) { - - transport::DataVector response; - response.reserve(model_length * 2); // this is a uint 8 vector - std::size_t max_regiser_read = everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE; - protocol_related_types::SunspecRegisterCount remaining_register_to_read{model_length}; - protocol_related_types::ModbusRegisterAddress read_address{m_base_address + model_address}; - - while (remaining_register_to_read > 0) { - std::size_t register_to_read = - remaining_register_to_read > max_regiser_read ? max_regiser_read : remaining_register_to_read; - - transport::DataVector tmp = - m_modbus_client.read_holding_register(m_unit_id, read_address.val, register_to_read); - response.insert(response.end(), tmp.begin(), tmp.end()); - - read_address.val += register_to_read; - remaining_register_to_read -= register_to_read; - } - - return response; -} - -transport::DataVector SerialCommHubTransport::fetch(protocol_related_types::SunspecDataModelAddress model_address, - protocol_related_types::SunspecRegisterCount model_length) { - - transport::DataVector response; - response.reserve(model_length * 2); // this is a uint 8 vector - std::size_t max_regiser_read = everest::modbus::consts::rtu::MAX_REGISTER_PER_MESSAGE; - protocol_related_types::SunspecRegisterCount remaining_register_to_read{model_length}; - protocol_related_types::ModbusRegisterAddress read_address{m_base_address + model_address}; - - while (remaining_register_to_read > 0) { - std::size_t register_to_read = - remaining_register_to_read > max_regiser_read ? max_regiser_read : remaining_register_to_read; - - types::serial_comm_hub_requests::Result serial_com_hub_result = - m_serial_hub.call_modbus_read_holding_registers(m_unit_id, read_address.val, register_to_read); - - if (not serial_com_hub_result.value.has_value()) - throw std::runtime_error("no result from serial com hub!"); - - // make sure that returned vector is a int32 vector - static_assert( - std::is_same_v); - - union { - int32_t val_32; - struct { - uint8_t v3; - uint8_t v2; - uint8_t v1; - uint8_t v0; - } val_8; - } swapit; - - static_assert(sizeof(swapit.val_32) == sizeof(swapit.val_8)); - - transport::DataVector tmp{}; - - for (auto item : serial_com_hub_result.value.value()) { - swapit.val_32 = item; - tmp.push_back(swapit.val_8.v2); - tmp.push_back(swapit.val_8.v3); - } - - response.insert(response.end(), tmp.begin(), tmp.end()); - - read_address.val += register_to_read; - remaining_register_to_read -= register_to_read; - } - - return response; -} - -transport::DataVector SerialCommHubTransport::fetch(const known_model::AddressData& ad) { - - return fetch(ad.base_offset, ad.model_size); -} - -bool SerialCommHubTransport::trigger_snapshot_generation( - protocol_related_types::ModbusRegisterAddress snapshot_trigger_register) { - - using namespace std::chrono_literals; - using namespace std::string_literals; - - types::serial_comm_hub_requests::VectorUint16 trigger_create_snapshot_command{{0x0002}}; - - types::serial_comm_hub_requests::StatusCodeEnum write_result = m_serial_hub.call_modbus_write_multiple_registers( - m_unit_id, snapshot_trigger_register.val, trigger_create_snapshot_command); - - if (not(types::serial_comm_hub_requests::StatusCodeEnum::Success == write_result)) - throw(""s + __PRETTY_FUNCTION__ + " SerialCommHub error : "s + - types::serial_comm_hub_requests::status_code_enum_to_string(write_result)); - - std::size_t counter = 10; - - while (counter-- > 0) { - - types::serial_comm_hub_requests::Result serial_com_hub_result = - m_serial_hub.call_modbus_read_holding_registers(m_unit_id, snapshot_trigger_register.val, true); - - if (not serial_com_hub_result.value.has_value()) - throw std::runtime_error("no result from serial com hub!"); - - auto snapshot_generatrion_code = serial_com_hub_result.value.value(); - - if ((not snapshot_generatrion_code.empty()) and (snapshot_generatrion_code[0] == 0)) - return true; - - std::this_thread::sleep_for(750ms); - } - - return false; -} - -bool SerialCommHubTransport::trigger_snapshot_generation_BSM() { - - return trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress(40525_sma)); -} - -bool SerialCommHubTransport::trigger_snapshot_generation_BSM_OCMF() { - - return trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress(41795_sma)); -} - -} // namespace transport diff --git a/modules/PowermeterBSM/lib/transport.hpp b/modules/PowermeterBSM/lib/transport.hpp deleted file mode 100644 index 14b72e8a1..000000000 --- a/modules/PowermeterBSM/lib/transport.hpp +++ /dev/null @@ -1,126 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_TRANSPORT_HPP -#define POWERMETER_TRANSPORT_HPP - -#include - -#include "known_model.hpp" -#include "protocol_related_types.hpp" - -#include - -/** - * Baseclass for transport classes. - * - * Transports are: - * - direct connection via modbus - * - connection via SerialComHub - */ - -#include - -namespace transport { - -class AbstractDataTransport { - -public: - using Spt = std::shared_ptr; - - /** - * Starting at SunspecDataModelAddress fetch Types::SunspecRegisterCount register contents. - */ - virtual transport::DataVector fetch(protocol_related_types::SunspecDataModelAddress, - protocol_related_types::SunspecRegisterCount) = 0; - - /** - * fetch data from a KnownModel (example: Sunspec Common ) - */ - virtual transport::DataVector fetch(const known_model::AddressData& ad) = 0; - - /** - * device specific: Trigger generation of a custom BSM signed snapshot. - */ - virtual bool trigger_snapshot_generation_BSM() = 0; - - /** - * device specific: Trigger generation of OCMF signed snapshot BSM power meter. - */ - virtual bool trigger_snapshot_generation_BSM_OCMF() = 0; -}; - -/** - * data transport via modbus. - */ -class ModbusTransport : public AbstractDataTransport { - -protected: - everest::connection::SerialDeviceConfiguration m_cfg; - everest::connection::SerialDevice m_device; - // everest::connection::SerialDeviceLogToStream m_device; - everest::connection::RTUConnection m_connection; - everest::modbus::ModbusRTUClient m_modbus_client; - protocol_related_types::ModbusUnitId m_unit_id; - protocol_related_types::ModbusRegisterAddress m_base_address; - - bool trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress snapshot_trigger_register); - -public: - ModbusTransport(std::string serial_device_name, protocol_related_types::ModbusUnitId unit_id, - protocol_related_types::ModbusRegisterAddress base_address); - - everest::connection::RTUConnection& rtu_connection() { - return m_connection; - } - - everest::modbus::ModbusRTUClient& modbus_client() { - return m_modbus_client; - } - - everest::connection::SerialDevice serial_device() { - return m_device; - } - - virtual transport::DataVector fetch(protocol_related_types::SunspecDataModelAddress, - protocol_related_types::SunspecRegisterCount) override; - - virtual transport::DataVector fetch(const known_model::AddressData& ad) override; - - virtual bool trigger_snapshot_generation_BSM() override; - - virtual bool trigger_snapshot_generation_BSM_OCMF() override; -}; - -/** - * data transport via SerialComHub - */ - -class SerialCommHubTransport : public AbstractDataTransport { - -protected: - serial_communication_hubIntf& m_serial_hub; - protocol_related_types::ModbusUnitId m_unit_id; - protocol_related_types::ModbusRegisterAddress m_base_address; - - bool trigger_snapshot_generation(protocol_related_types::ModbusRegisterAddress); - -public: - SerialCommHubTransport(serial_communication_hubIntf& serial_hub, protocol_related_types::ModbusUnitId unit_id, - protocol_related_types::ModbusRegisterAddress base_address) : - m_serial_hub(serial_hub), m_unit_id(unit_id), m_base_address(base_address) { - } - - virtual transport::DataVector fetch(protocol_related_types::SunspecDataModelAddress, - protocol_related_types::SunspecRegisterCount) override; - - virtual transport::DataVector fetch(const known_model::AddressData& ad) override; - - virtual bool trigger_snapshot_generation_BSM() override; - - virtual bool trigger_snapshot_generation_BSM_OCMF() override; -}; - -} // namespace transport - -#endif // POWERMETER_TRANSPORT_HPP diff --git a/modules/PowermeterBSM/main/powermeterImpl.cpp b/modules/PowermeterBSM/main/powermeterImpl.cpp deleted file mode 100644 index c7963c910..000000000 --- a/modules/PowermeterBSM/main/powermeterImpl.cpp +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest - -#include "powermeterImpl.hpp" - -#include "lib/transport.hpp" -#include - -#include -#include -#include -#include - -using namespace std::chrono_literals; - -namespace module { -namespace main { - -////////////////////////////////////////////////////////////////////// -// -// module related stuff - -void powermeterImpl::init() { - EVLOG_verbose << "init: "; - m_module_start_timestamp = - std::chrono::time_point_cast(std::chrono::system_clock::now()).time_since_epoch(); -} - -void powermeterImpl::ready() { - - m_watchdog_thread = std::thread(&powermeterImpl::watchdog, this); -} - -types::powermeter::TransactionStopResponse powermeterImpl::handle_stop_transaction(std::string& transaction_id) { - try { - - std::scoped_lock lock(mod->get_device_mutex()); - - transport::AbstractDataTransport::Spt transport = mod->get_data_transport(); - - if (not transport->trigger_snapshot_generation_BSM_OCMF()) - EVLOG_debug << __PRETTY_FUNCTION__ << " trigger for OCMF signed snapshot failed! " << std::endl; - - transport::DataVector data = transport->fetch(known_model::BSM_OCMF_CurrentSnapshot); - - bsm::SignedOCMFSnapshot signed_snapshot(data); - auto signed_meter_value = types::units_signed::SignedMeterValue{signed_snapshot.O(), "", "OCMF"}; - - return {types::powermeter::TransactionRequestStatus::OK, signed_meter_value}; - - } catch (const std::runtime_error& e) { - EVLOG_error << __PRETTY_FUNCTION__ << " Error: " << e.what() << std::endl; - return {types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR, {}, {}, "get_signed_meter_value_error"}; - } -}; - -// FIXME: probably this would need to be handled differently in Bauer power meter - need to actually start a transaction -// on the power meter. With current implementation we do not get a start signed meter value -types::powermeter::TransactionStartResponse -powermeterImpl::handle_start_transaction(types::powermeter::TransactionReq& value) { - return {types::powermeter::TransactionRequestStatus::OK}; -} - -////////////////////////////////////////////////////////////////////// -// -// module module implementation - -void powermeterImpl::watchdog() { - - // poor mans watchdog - m_publish_variables_worker_thread = std::thread(&powermeterImpl::worker, this); - - while (true) { - - std::this_thread::sleep_for(mod->get_watchdog_interval()); - - if (m_publish_variables_worker_thread.joinable()) { - EVLOG_warning << __PRETTY_FUNCTION__ << " worker thread has died. restarting... " << std::endl; - m_publish_variables_worker_thread.join(); - m_publish_variables_worker_thread = std::thread(&powermeterImpl::worker, this); - } - } - - EVLOG_error << __PRETTY_FUNCTION__ << " watchdog thread terminating, module is dead." << std::endl; -} - -void powermeterImpl::worker() { - - try { - - EVLOG_verbose << __PRETTY_FUNCTION__ << " start " << std::endl; - - mod->dump_config_to_log(); - - while (true) { - - std::this_thread::sleep_for(mod->get_update_interval()); - - try { - - // lock device mutex - std::scoped_lock lock(mod->get_device_mutex()); - - transport::AbstractDataTransport::Spt transport = mod->get_data_transport(); - - EVLOG_debug << __PRETTY_FUNCTION__ << " wakeup. " << std::endl; - - transport::DataVector data = transport->fetch(known_model::Sunspec_ACMeter); - - sunspec_model::ACMeter acmeter(data); - types::powermeter::Powermeter result; - - result.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); - - result.meter_id = mod->config.meter_id; - - float scale_factor_Wh_import = pow(10, acmeter.TotWh_SF()); - result.energy_Wh_import.total = acmeter.TotWhIm() * scale_factor_Wh_import; - - float scale_factor_W = pow(10, acmeter.W_SF()); - result.power_W = types::units::Power{.total = static_cast(acmeter.W() * scale_factor_W)}; - - float scale_factor_current = pow(10, acmeter.A_SF()); - result.current_A = types::units::Current{.L1 = static_cast(acmeter.A() * scale_factor_current)}; - - float scale_factor_voltage = pow(10, acmeter.V_SF()); - result.voltage_V = - types::units::Voltage{.L1 = static_cast(acmeter.PhVphA() * scale_factor_voltage), - .L2 = static_cast(acmeter.PhVphB() * scale_factor_voltage), - .L3 = static_cast(acmeter.PhVphC() * scale_factor_voltage)}; - - float scale_factor_frequency = pow(10, acmeter.Hz_SF()); - result.frequency_Hz = - types::units::Frequency{.L1 = static_cast(acmeter.Hz() * scale_factor_frequency)}; - - float scale_factor_reactive_power = pow(10, acmeter.VAR_SF()); - result.VAR = types::units::ReactivePower{ - .total = static_cast(acmeter.VAR() * scale_factor_reactive_power), - .L1 = static_cast(acmeter.VARphA() * scale_factor_reactive_power), - .L2 = static_cast(acmeter.VARphB() * scale_factor_reactive_power), - .L3 = static_cast(acmeter.VARphC() * scale_factor_reactive_power)}; - - publish_powermeter(result); - - } catch (const std::exception& e) { - // we catch std::exception& here since there may be other exceotions than std::runtime_error - EVLOG_warning << __PRETTY_FUNCTION__ << " Exception caught: " << e.what() << std::endl; - } - } - } catch (const std::runtime_error& e) { - EVLOG_warning << __PRETTY_FUNCTION__ << " workerthread dies: " << e.what() << std::endl; - } -} - -} // namespace main -} // namespace module diff --git a/modules/PowermeterBSM/main/powermeterImpl.hpp b/modules/PowermeterBSM/main/powermeterImpl.hpp deleted file mode 100644 index ee4cfc40e..000000000 --- a/modules/PowermeterBSM/main/powermeterImpl.hpp +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Pionix GmbH and Contributors to EVerest -#ifndef MAIN_POWERMETER_IMPL_HPP -#define MAIN_POWERMETER_IMPL_HPP - -// -// AUTO GENERATED - MARKED REGIONS WILL BE KEPT -// template version 3 -// - -#include - -#include "../PowermeterBSM.hpp" - -// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 -// insert your custom include headers here -#include -#include -#include -#include -// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 - -namespace module { -namespace main { - -struct Conf {}; - -class powermeterImpl : public powermeterImplBase { -public: - powermeterImpl() = delete; - powermeterImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : - powermeterImplBase(ev, "main"), mod(mod), config(config){}; - - // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 - // insert your public definitions here - // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 - -protected: - // command handler functions (virtual) - virtual types::powermeter::TransactionStartResponse - handle_start_transaction(types::powermeter::TransactionReq& value) override; - virtual types::powermeter::TransactionStopResponse handle_stop_transaction(std::string& transaction_id) override; - - // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 - // insert your protected definitions here - std::thread m_publish_variables_worker_thread; - std::thread m_watchdog_thread; - - // FIXME: this is just a nonsense workaround timestamp - std::chrono::seconds m_module_start_timestamp; - protocol_related_types::ModbusUnitId m_unit_id{}; - std::string m_serial_device_name; - protocol_related_types::ModbusRegisterAddress m_modbus_base_address{0}; - - void worker(); - void watchdog(); - // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 - -private: - const Everest::PtrContainer& mod; - const Conf& config; - - virtual void init() override; - virtual void ready() override; - - // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 - // insert your private definitions here - // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 -}; - -// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 -// insert other definitions here -// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 - -} // namespace main -} // namespace module - -#endif // MAIN_POWERMETER_IMPL_HPP diff --git a/modules/PowermeterBSM/manifest.yaml b/modules/PowermeterBSM/manifest.yaml deleted file mode 100644 index ea6a8faad..000000000 --- a/modules/PowermeterBSM/manifest.yaml +++ /dev/null @@ -1,58 +0,0 @@ -description: Module that collects power and energy measurements from a MODBUS RTU - device -provides: - main: - description: This is the main unit of the module - interface: powermeter - ac_meter: - description: sunspec ac meter - interface: sunspec_ac_meter -config: - power_unit_id: - description: Modbus unit_id, mostly 1 - type: integer - minimum: 1 - maximum: 255 - sunspec_base_address: - description: sunspec base address of device ( 0, 40000 or 50000 ) - type: integer - default: 40000 - update_interval: - description: Update interval in seconds. - type: integer - minimum: 1 - watchdog_wakeup_interval: - description: wakup interval of watchdog in seconds (default 60 seconds). - type: integer - minimum: 1 - default: 60 - serial_device: - description: Serial port the BSM hardware is connected to - type: string - default: /dev/ttyUSB0 - baud: - description: 'Baud rate on RS-485, allowed value range: 2400 115200 (19200 is - default)' - type: integer - minimum: 2400 - maximum: 115200 - default: 19200 - use_serial_comm_hub: - description: When enabled, use a serial serial_communication_hub, otherwise use - the configured serial device. - type: boolean - default: true - meter_id: - description: Arbitrary string id, used as power_meter_id in interface powermeter. - type: string - default: no_meter_id -requires: - serial_com_0_connection: - interface: serial_communication_hub - min_connections: 0 - max_connections: 1 -enable_external_mqtt: true -metadata: - license: https://opensource.org/licenses/Apache-2.0 - authors: - - Christoph Kliemt diff --git a/modules/PowermeterBSM/tests/CMakeLists.txt b/modules/PowermeterBSM/tests/CMakeLists.txt deleted file mode 100644 index 06aba30fb..000000000 --- a/modules/PowermeterBSM/tests/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# Copyright Pionix GmbH and Contributors to EVerest -set(TEST_TARGET_NAME ${PROJECT_NAME}_test_rtu_device) -add_executable( ${TEST_TARGET_NAME} test_rtu_device.cpp test_helper.cpp ) -target_link_libraries( ${TEST_TARGET_NAME} - sunspec_framework_object_lib - GTest::gtest_main - GTest::gmock - ) -add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) - -set(TEST_TARGET_NAME ${PROJECT_NAME}_test_snapshot_models) -add_executable( ${TEST_TARGET_NAME} test_snapshot_models.cpp ) -target_link_libraries( ${TEST_TARGET_NAME} - sunspec_framework_object_lib - GTest::gtest_main - GTest::gmock - ) -add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME}) diff --git a/modules/PowermeterBSM/tests/test_helper.cpp b/modules/PowermeterBSM/tests/test_helper.cpp deleted file mode 100644 index 9053cbb93..000000000 --- a/modules/PowermeterBSM/tests/test_helper.cpp +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#include "test_helper.hpp" - -namespace test_helper { - -bool check_sunspec_device_base_regiser(everest::modbus::ModbusRTUClient& modbus_client, - protocol_related_types::ModbusUnitId unit_id, - protocol_related_types::ModbusRegisterAddress register_to_test) { - const everest::modbus::DataVectorUint8 SunSMarker{0x53, 0x75, 0x6e, 0x53}; - try { - return SunSMarker == modbus_client.read_holding_register(unit_id, register_to_test.val, 2); - } catch (const std::runtime_error& e) { - // std::cout << e.what() << std::flush; - } - return false; -} - -protocol_related_types::ModbusRegisterAddress -get_sunspec_device_base_register(everest::connection::SerialDevice& serialdevice, - protocol_related_types::ModbusUnitId unit_id) { - - everest::connection::RTUConnection connection(serialdevice); - everest::modbus::ModbusRTUClient modbus_client(connection); - - return get_sunspec_device_base_register(modbus_client, unit_id); -} - -protocol_related_types::ModbusRegisterAddress -get_sunspec_device_base_register(everest::modbus::ModbusRTUClient& modbus_client, - protocol_related_types::ModbusUnitId unit_id) { - // The beginning of the device Modbus map MUST be located at one of three Modbus addresses - // in the Modbus holding register address space: 0, 40000 (0x9C40) or 50000 (0xC350). These - // Modbus addresses are the full 16-bit, 0-based addresses in the Modbus protocol messages. - - protocol_related_types::ModbusRegisterAddress register_to_test[]{0_mra, 40000_mra, 50000_mra}; - - for (protocol_related_types::ModbusRegisterAddress mra : register_to_test) - if (check_sunspec_device_base_regiser(modbus_client, unit_id, mra)) - return mra; - - throw std::runtime_error("not a sunspec device!"); -} - -} // namespace test_helper diff --git a/modules/PowermeterBSM/tests/test_helper.hpp b/modules/PowermeterBSM/tests/test_helper.hpp deleted file mode 100644 index 12c7b92c3..000000000 --- a/modules/PowermeterBSM/tests/test_helper.hpp +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#ifndef POWERMETER_BSM_DRIVER_BASE_HPP -#define POWERMETER_BSM_DRIVER_BASE_HPP - -#include - -#include "lib/protocol_related_types.hpp" - -/** - * sunpec related helper tools. - */ -namespace test_helper { - -// returns true if the tested register base is a sunspec base register -bool check_sunspec_device_base_regiser(everest::modbus::ModbusRTUClient& modbus_client, - protocol_related_types::ModbusUnitId unit_id, - protocol_related_types::ModbusRegisterAddress register_to_test); - -protocol_related_types::ModbusRegisterAddress -get_sunspec_device_base_register(everest::connection::SerialDevice& serialdevice, - protocol_related_types::ModbusUnitId unit_id); - -// throws std::runtime_error if no baseregister could be detected. -protocol_related_types::ModbusRegisterAddress get_sunspec_device_base_register(everest::modbus::ModbusRTUClient&, - protocol_related_types::ModbusUnitId); - -} // namespace test_helper - -#endif // POWERMETER_BSM_DRIVER_BASE_HPP diff --git a/modules/PowermeterBSM/tests/test_rtu_device.cpp b/modules/PowermeterBSM/tests/test_rtu_device.cpp deleted file mode 100644 index ef8d2cc4d..000000000 --- a/modules/PowermeterBSM/tests/test_rtu_device.cpp +++ /dev/null @@ -1,148 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#include -#include - -#include "test_helper.hpp" - -#include "lib/sunspec_base.hpp" // helper stuff for sunspec model definition / handling - -#include "lib/BSMSnapshotModel.hpp" -#include "lib/sunspec_models.hpp" // sunspec models - -#include "lib/known_model.hpp" -#include "lib/transport.hpp" - -#include - -#include -#include -#include - -using namespace std::chrono_literals; -using namespace std::string_literals; - -// we are only using function codes read / write multiple registers -using ModbusUnitId = protocol_related_types::ModbusUnitId; - -struct TestBSMPowerMeter : public ::testing::Test { - - std::string serial_device_name{"/dev/ttyUSB0"}; - everest::connection::SerialDeviceConfiguration cfg; - everest::connection::SerialDeviceLogToStream serial_device; - everest::connection::RTUConnection rtu_connection; - everest::modbus::ModbusRTUClient modbus_client; - - std::string current_test_name; - std::string current_test_suite; - std::fstream logstream; - - const ModbusUnitId unit_id = 42; - const protocol_related_types::ModbusRegisterAddress default_sunspec_base_address{40000}; - - TestBSMPowerMeter() : - cfg(serial_device_name), serial_device(cfg), rtu_connection(serial_device), modbus_client(rtu_connection){}; - - void SetUp() override { - - current_test_name = ::testing::UnitTest::GetInstance()->current_test_info()->name(); - current_test_suite = ::testing::UnitTest::GetInstance()->current_test_suite()->name(); - std::cout << __PRETTY_FUNCTION__ << " " << current_test_suite << " " << current_test_name << "\n"; - std::string logfile_name = current_test_suite + "_" + current_test_name + ".log"; - logstream.open(logfile_name, std::fstream::out); - serial_device.set_stream(&logstream); - } - - void TearDown() override { - std::cout << __PRETTY_FUNCTION__ << std::endl; - logstream.close(); - } -}; - -TEST_F(TestBSMPowerMeter, DriverBaseCheckSunspecDevice) { - - // verify that a sunspec compatilbe device is connected - // throws if no device found - ASSERT_NO_THROW(test_helper::get_sunspec_device_base_register(modbus_client, unit_id)); -} - -TEST_F(TestBSMPowerMeter, DriverBaseCheckBSMWS36A) { - - // verify that a BSM-WS36A-H01-1311-0000 device is connected - protocol_related_types::ModbusRegisterAddress base_address = - test_helper::get_sunspec_device_base_register(serial_device, unit_id); - - transport::AbstractDataTransport::Spt fetcher_spt = - std::make_shared(serial_device_name, unit_id, base_address); - - { - transport::DataVector dv = fetcher_spt->fetch(3_sma, calc_model_size_in_register(sunspec_model::Common::Model)); - sunspec_model::Common common(dv); - EXPECT_EQ(common.Mn(), "BAUER Electronic"); - EXPECT_EQ(common.Md(), "BSM-WS36A-H01-1311-0000"); - } - - { - transport::DataVector dv = fetcher_spt->fetch(known_model::Sunspec_Common); - sunspec_model::Common common(dv); - EXPECT_EQ(common.Mn(), "BAUER Electronic"); - EXPECT_EQ(common.Md(), "BSM-WS36A-H01-1311-0000"); - } -} - -TEST_F(TestBSMPowerMeter, ReadBSMSnapshot) { - - transport::AbstractDataTransport::Spt transport_spt = - std::make_shared(serial_device_name, unit_id, default_sunspec_base_address); - - for (std::size_t counter = 0; counter < 10; counter++) { - - ASSERT_TRUE(transport_spt->trigger_snapshot_generation_BSM()); - transport::DataVector dv = transport_spt->fetch(known_model::BSM_CurrentSnapshot); - bsm::SignedSnapshot snapshot(dv); - - std::cout << std::dec; - - model_to_stream(std::cout, snapshot, - {5, // watt - 8, // response counter - 9, // Operation seconds - 3} // Total Wh Exp - ); - - std::this_thread::sleep_for(5000ms); - } -} - -TEST_F(TestBSMPowerMeter, ReadBSM_OCMFSnapshot) { - - transport::AbstractDataTransport::Spt transport_spt = - std::make_shared(serial_device_name, unit_id, default_sunspec_base_address); - - for (std::size_t counter = 0; counter < 10; counter++) { - - ASSERT_TRUE(transport_spt->trigger_snapshot_generation_BSM_OCMF()); - transport::DataVector dv = transport_spt->fetch(known_model::BSM_OCMF_CurrentSnapshot); - bsm::SignedOCMFSnapshot snapshot(dv); - - std::cout << std::dec; - - // { "ID", PointType::uint16 }, - // { "L", PointType::uint16 }, - // { "Type", PointType::enum16 }, - // { "St", PointType::enum16 }, - // { "O", PointType::string , 496 } - - model_to_stream(std::cout, snapshot, - { - 0, // ID - 1, // L - 2, // type - 3, // status - 4, // OCMF result string - }); - - std::this_thread::sleep_for(5000ms); - } -} diff --git a/modules/PowermeterBSM/tests/test_snapshot_models.cpp b/modules/PowermeterBSM/tests/test_snapshot_models.cpp deleted file mode 100644 index a50776bb1..000000000 --- a/modules/PowermeterBSM/tests/test_snapshot_models.cpp +++ /dev/null @@ -1,646 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest - -#include "lib/BSMSnapshotModel.hpp" -#include "lib/protocol_related_types.hpp" -#include "lib/sunspec_models.hpp" - -#include -#include - -#include -#include -#include - -// clang-format off -constexpr PointInitializerList init_list = { - { "acc32" , PointType::acc32 }, - { "bitfield32", PointType::bitfield32 } , - { "enum16", PointType::enum16 , 70 }, // just a test, this should be ignored. - { "int16" , PointType::int16 }, - { "pad" , PointType::pad , 1 }, - { "sunsf" , PointType::uint16 }, - { "uint32", PointType::uint32 }, - { "string", PointType::string , 70 }, - { "dummy" , PointType::acc32 }, -}; -// clang-format on - -constexpr PointArray model(calc_offset(init_list)); - -using SunspecModelBaseTest = SunspecModelBase; - -TEST(TestTypes, AddressTypes) { - - // test user defined literal stuff. - - protocol_related_types::ModbusRegisterAddress m0{10_mra}; - protocol_related_types::ModbusRegisterAddress m1{10_sma}; - - EXPECT_EQ(m0.val, 10); - EXPECT_EQ(m1.val, 9); - - protocol_related_types::SunspecDataModelAddress s0{10_mra}; - protocol_related_types::SunspecDataModelAddress s1{10_sma}; - - EXPECT_EQ(s0.val, 11); - EXPECT_EQ(s1.val, 10); - - s0 = m0; - EXPECT_EQ(s0.val, 11); - - m0 = s0; - EXPECT_EQ(m0.val, 10); - - protocol_related_types::SunspecDataModelAddress sdm_address{10}; - protocol_related_types::ModbusRegisterAddress mr_address{100}; - - protocol_related_types::ModbusRegisterAddress mb_result_address{mr_address + sdm_address}; - EXPECT_EQ(mb_result_address.val, 109); - - protocol_related_types::SunspecDataModelAddress sdb_result_address{mr_address + sdm_address}; - EXPECT_EQ(sdb_result_address.val, 110); -} - -TEST(TestSnapshotModels, calc_offset) { - - // verify init of offset in models - - EXPECT_EQ(model.size(), init_list.size()); - EXPECT_EQ(model.at(0).offset, 0); // acc32 2 - EXPECT_EQ(model.at(1).offset, 4); // bitfield32 2 - EXPECT_EQ(model.at(2).offset, 8); // enum16 1 - EXPECT_EQ(model.at(3).offset, 10); // int16 1 - EXPECT_EQ(model.at(4).offset, 12); // pad 1 - EXPECT_EQ(model.at(5).offset, 14); // sunsf 1 - EXPECT_EQ(model.at(6).offset, 16); // uint32 2 - EXPECT_EQ(model.at(7).offset, 20); // string - EXPECT_EQ(model.at(8).offset, 160); // dummy -} - -TEST(TestSnapshotModels, TestModelLength) { - - // check that calc_model_size_in_bytes works correctly - static_assert(calc_model_size_in_bytes(model) == 164); -} - -TEST(TestSnapshotModels, TestBSMSnapshotModelData) { - - EXPECT_EQ(bsm::SignedSnapshot::Model.size(), bsm::BSMSnapshotPointInitList.size()); - EXPECT_EQ(bsm::SignedSnapshot::Model.at(0).offset, 0); // e16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(1).offset, 2); // e16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(2).offset, 4); // acc32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(3).offset, 8); // acc32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(4).offset, 12); // sunssf - EXPECT_EQ(bsm::SignedSnapshot::Model.at(5).offset, 14); // int16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(6).offset, 16); // sunssf - EXPECT_EQ(bsm::SignedSnapshot::Model.at(7).offset, 18); // string 8 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(8).offset, 34); // uint32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(9).offset, 38); // uint32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(10).offset, 42); // uint32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(11).offset, 46); // int16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(12).offset, 48); // uint32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(13).offset, 52); // uint32 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(14).offset, 56); // uint16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(15).offset, 58); // uint16 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(16).offset, 60); // string 70 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(17).offset, 200); // string 50 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(18).offset, 300); // string 50 - EXPECT_EQ(bsm::SignedSnapshot::Model.at(19).offset, 400); // bitfield32 -} - -transport::DataVector create_test_data(std::initializer_list il, - std::size_t result_size) { - transport::DataVector result(result_size); - - auto it = result.begin(); - for (auto item : il) - (*it++) = item; - - return result; -} - -TEST(TestSunspecModelBase, UnsignedConvsersion) { - - // we fake a vector with the size of a respone DataVector from modbus that is large enough for the model in - // question. - transport::DataVector data{create_test_data({0xff, 0xee, 0xdd, 0xcc, 0x00}, calc_model_size_in_bytes(model))}; - - std::uint32_t val32{0xffeeddcc}; - SunspecModelBaseTest smb(data); - EXPECT_EQ(smb.uint32_at(0), val32); - - std::uint16_t val16{0xeedd}; - EXPECT_EQ(smb.uint16_at(1), val16); -} - -TEST(TestSunspecModelBase, SignedConvsersion) { - - { - transport::DataVector signed_data{create_test_data({0xfb, 0x2e}, calc_model_size_in_bytes(model))}; // -1234 - SunspecModelBaseTest smb(signed_data); - - std::int16_t result{-1234}; - - EXPECT_EQ(smb.int16_at(0), result); - } - - { - transport::DataVector signed_data{create_test_data({0x80, 0x00}, calc_model_size_in_bytes(model))}; - SunspecModelBaseTest smb(signed_data); - - std::int16_t result{std::numeric_limits::min()}; - - EXPECT_EQ(smb.int16_at(0), result); - } - - { - transport::DataVector signed_data{create_test_data({0x7f, 0xff}, calc_model_size_in_bytes(model))}; - SunspecModelBaseTest smb(signed_data); - - std::int16_t result{std::numeric_limits::max()}; - - EXPECT_EQ(smb.int16_at(0), result); - } - - { - transport::DataVector signed_data{create_test_data({0x00, 0x00}, calc_model_size_in_bytes(model))}; - SunspecModelBaseTest smb(signed_data); - - std::int16_t result{0}; - - EXPECT_EQ(smb.int16_at(0), result); - } -} - -TEST(TestSunspecModelBase, StringConversion) { - - transport::DataVector string_data{ - create_test_data({'H', 'e', 'r', 'b', 'e', 'r', 't'}, calc_model_size_in_bytes(model))}; - std::string result{"Herbert"}; - SunspecModelBaseTest smb(string_data); - EXPECT_EQ(smb.string_at_with_length(0, 7), result); -} - -TEST(TestSunspecModelBaseTest, EmptyContainer) { - - transport::DataVector empty; - - ASSERT_THROW(sunspec_model::Common sc(empty), std::runtime_error); -} - -// clang-format off -/* - * snapshot status regiser is 40524 => 9E4C - * ->bsmtool --verbose --trace --device /dev/ttyUSB0 get-snapshot signed_current_snapshot -/dev/ttyUSB0:42[addr=40524] ->2A109E4C0001020002BCA5 ==> write update into register -/dev/ttyUSB0:42[addr=40524] <--2A109E4C0001E9ED ==> return snapshot is valid -/dev/ttyUSB0:42[addr=40523] ->2A039E4B00641DC4 ==> read data -/dev/ttyUSB0:42[addr=40523] <--2A 03 C800 0000 0000 0015 AE00 001BB2000000000001303031425A5231353231303730303139000001640016EFAAFFFFFFFF8000000000390016A42C000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000F3E2 -/dev/ttyUSB0:42[addr=40623] ->2A039EAF007D9C39 ==> continue read data -/dev/ttyUSB0:42[addr=40623] <--2A03FA000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000473045022040917A89270AD73DBFCFAF187C592DF0962E2BABBCC1C6A8D4456B5DB5E76BD6022100E1652CD40D -/dev/ttyUSB0:42[addr=40748] ->2A039F2C001BEC07 ==> continue read data -/dev/ttyUSB0:42[addr=40748] <--2A03360A6CDB7F9C732654DA09286730C16FE9EC853687523CBFF459F87AA9030000000000000000000000000000000000000000000000000057B6 -Updating 'signed_current_snapshot' succeeded -Snapshot data: -bsm_snapshot: - ID: 64901 - L: 252 - fixed: - Typ: 0 - St: 0 - RCR: 5550 Wh - TotWhImp: 7090 Wh - W: 0.0 W - MA1: 001BZR1521070019 - RCnt: 356 - OS: 1503146 s - Epoch: None - TZO: None - EpochSetCnt: 57 - EpochSetOS: 1483820 s - DI: 0 - DO: 0 - Meta1: None - Meta2: None - Meta3: None - Evt: 0 - NSig: 48 - BSig: 71 - repeating blocks blob: - Sig: 3045022040917a89270ad73dbfcfaf187c592df0962e2babbcc1c6a8d4456b5db5e76bd6022100e1652c0a6cdb7f9c732654da09286730c16fe9ec853687523cbff459f87aa903 - - */ -// clang-format on - -TEST(TestModel, BSMSignedSnapshot) { - - // device id, function code, number of bytes ... crc crc - - transport::DataVector data = { - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0xAE, 0x00, 0x00, 0x1B, 0xB2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x30, 0x30, 0x31, 0x42, 0x5A, 0x52, 0x31, 0x35, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x00, 0x00, - 0x01, 0x64, 0x00, 0x16, 0xEF, 0xAA, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x39, 0x00, 0x16, - 0xA4, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x47, 0x30, 0x45, 0x02, 0x20, 0x40, 0x91, - 0x7A, 0x89, 0x27, 0x0A, 0xD7, 0x3D, 0xBF, 0xCF, 0xAF, 0x18, 0x7C, 0x59, 0x2D, 0xF0, 0x96, 0x2E, 0x2B, 0xAB, - 0xBC, 0xC1, 0xC6, 0xA8, 0xD4, 0x45, 0x6B, 0x5D, 0xB5, 0xE7, 0x6B, 0xD6, 0x02, 0x21, 0x00, 0xE1, 0x65, 0x2C, - 0x0A, 0x6C, 0xDB, 0x7F, 0x9C, 0x73, 0x26, 0x54, 0xDA, 0x09, 0x28, 0x67, 0x30, 0xC1, 0x6F, 0xE9, 0xEC, 0x85, - 0x36, 0x87, 0x52, 0x3C, 0xBF, 0xF4, 0x59, 0xF8, 0x7A, 0xA9, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - - bsm::SignedSnapshot signed_snapshot(data); - - EXPECT_EQ(signed_snapshot.Type(), 0); - - EXPECT_EQ(signed_snapshot.Status(), 0); - - EXPECT_EQ(signed_snapshot.RCR(), 5550); - - EXPECT_EQ(signed_snapshot.TotWhImp(), 7090); - - EXPECT_EQ(signed_snapshot.W(), 0); - - EXPECT_EQ(signed_snapshot.MA1(), "001BZR1521070019"); - - EXPECT_EQ(signed_snapshot.RCnt(), 356); - - EXPECT_EQ(signed_snapshot.OS(), 1503146); - - EXPECT_EQ(signed_snapshot.Epoch(), invalid_point_value::uint32); - - EXPECT_EQ(signed_snapshot.TZO(), invalid_point_value::int16); - - EXPECT_EQ(signed_snapshot.EpochSetCnt(), 57); - - EXPECT_EQ(signed_snapshot.EpochSetOS(), 1483820); - - EXPECT_EQ(signed_snapshot.DI(), 0); - - EXPECT_EQ(signed_snapshot.DO(), 0); - - EXPECT_FALSE(invalid_point_value::valid_string(signed_snapshot.Meta1())); - - EXPECT_FALSE(invalid_point_value::valid_string(signed_snapshot.Meta2())); - - EXPECT_FALSE(invalid_point_value::valid_string(signed_snapshot.Meta3())); - - EXPECT_EQ(signed_snapshot.Evt(), 0); - - EXPECT_EQ(signed_snapshot.NSig(), 48); - - EXPECT_EQ(signed_snapshot.BSig(), 71); - - { - std::string expected{"3045022040917a89270ad73dbfcfaf187c592df0962e2babbcc1c6a8d4456b5db5e76bd6022100e1652c0a6cd" - "b7f9c732654da09286730c16fe9ec853687523cbff459f87aa903"}; - std::string converted{signed_snapshot.Sig()}; - EXPECT_EQ(expected, converted); - } - - { - std::string expected{ - "3045022040917a89270ad73dbfcfaf187c592df0962e2babbcc1c6a8d4456b5db5e76bd6022100e1652c0a6cdb7f9c732654da0928" - "6730c16fe9ec853687523cbff459f87aa90300000000000000000000000000000000000000000000000000"}; - std::string converted{signed_snapshot.SigPadded()}; - EXPECT_EQ(expected, converted); - } - - // model_to_stream(std::cout, signed_snapshot, { 5, // watt - // 8, // response counter - // 9, // Operation seconds - // } ); -} - -TEST(TestModel, BSMOCMFSignedSnapshot) { - - // clang-format off -/* ->bsmtool --trace --verbose --device /dev/ttyUSB0 get oscs -/dev/ttyUSB0:42[addr=41793] ->2A03A3410002B040 -/dev/ttyUSB0:42[addr=41793] <--2A0304000000006131 -/dev/ttyUSB0:42[addr=41795] ->2A03a343007D5060 ==> Start reading at modbus address 41795, sunspec address 41795 -/dev/ttyUSB0:42[addr=41795] <--2A0316#16FA4F434D467C7B224656223A22312E30222C224749223A22424155455220456C656374726F6E69632042534D2D57533336412D4830312D313331312D30303030222C224753223A22303031425A5231353231303730303139222C224756223A22312E393A333243413A414646342C2036643164643363222C225047223A2254353635222C224D56223A22424155455220456C656374726F6E6963222C224D4D223A2242534D2D57533336412D4830312D313331312D30303030222C224D53223A22303031425A5231353231303730303139222C224953223A66616C73652C224954223A22554E444546494E4544222C224944223A22222C22524422396A -/dev/ttyUSB0:42[addr=41920] ->2A03A3C0007DA188 -/dev/ttyUSB0:42[addr=41920] <--2A03FA3A5B7B22544D223A22313937302D30312D30315430303A30303A30302C3030302B303030302055222C225458223A2243222C225256223A313934302C225249223A22312D303A312E382E302A313938222C225255223A225768222C225856223A393034302C225849223A22312D303A312E382E302A323535222C225855223A225768222C225854223A302C225254223A224143222C224546223A22222C225354223A2247227D5D7D7C7B225341223A2245434453412D7365637032353672312D534841323536222C225344223A2233303434303232303236653162333033333364353762313537633339353934343964643766623536323630365ED7 -/dev/ttyUSB0:42[addr=42045] ->2A03A43D007D310C -/dev/ttyUSB0:42[addr=42045] <--2A03FA623436393232643463373834663862366165316664313333633566363032323036303666623735306133653864363334303932333737333365623762623464353634653238633731343633303531343362643832383561666331623330643165227D00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009EFF -/dev/ttyUSB0:42[addr=42170] ->2A03A4BA007980E6 -/dev/ttyUSB0:42[addr=42170] <--2A03F20000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000443C - - - -bsm_ocmf: - ID: 64903 - L: 498 - Typ: 0 - St: 0 - O: OCMF|{"FV":"1.0","GI":"BAUER Electronic BSM-WS36A-H01-1311-0000","GS":"001BZR1521070019","GV":"1.9:32CA:AFF4, 6d1dd3c","PG":"T565","MV":"BAUER Electronic","MM":"BSM-WS36A-H01-1311-0000","MS":"001BZR1521070019","IS":false,"IT":"UNDEFINED","ID":"","RD":[{"TM":"1970-01-01T00:00:00,000+0000 U","TX":"C","RV":1940,"RI":"1-0:1.8.0*198","RU":"Wh","XV":9040,"XI":"1-0:1.8.0*255","XU":"Wh","XT":0,"RT":"AC","EF":"","ST":"G"}]}|{"SA":"ECDSA-secp256r1-SHA256","SD":"3044022026e1b30333d57b157c3959449dd7fb562606b46922d4c784f8b6ae1fd133c5f60220606fb750a3e8d63409237733eb7bb4d564e28c7146305143bd8285afc1b30d1e"} - - */ - - // clang-format on - - transport::DataVector data = { - 0xFD, 0x87, // added sunspec model number - - 0x01, 0x74, // default payload length - - 0x00, 0x00, // snapshot type - - 0x00, 0x00, // status - - 0x4F, 0x43, 0x4D, 0x46, 0x7C, 0x7B, 0x22, 0x46, 0x56, 0x22, 0x3A, 0x22, 0x31, 0x2E, 0x30, 0x22, 0x2C, 0x22, - 0x47, 0x49, 0x22, 0x3A, 0x22, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, - 0x6E, 0x69, 0x63, 0x20, 0x42, 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, - 0x31, 0x33, 0x31, 0x31, 0x2D, 0x30, 0x30, 0x30, 0x30, 0x22, 0x2C, 0x22, 0x47, 0x53, 0x22, 0x3A, 0x22, 0x30, - 0x30, 0x31, 0x42, 0x5A, 0x52, 0x31, 0x35, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x22, 0x2C, 0x22, - 0x47, 0x56, 0x22, 0x3A, 0x22, 0x31, 0x2E, 0x39, 0x3A, 0x33, 0x32, 0x43, 0x41, 0x3A, 0x41, 0x46, 0x46, 0x34, - 0x2C, 0x20, 0x36, 0x64, 0x31, 0x64, 0x64, 0x33, 0x63, 0x22, 0x2C, 0x22, 0x50, 0x47, 0x22, 0x3A, 0x22, 0x54, - 0x35, 0x36, 0x35, 0x22, 0x2C, 0x22, 0x4D, 0x56, 0x22, 0x3A, 0x22, 0x42, 0x41, 0x55, 0x45, 0x52, 0x20, 0x45, - 0x6C, 0x65, 0x63, 0x74, 0x72, 0x6F, 0x6E, 0x69, 0x63, 0x22, 0x2C, 0x22, 0x4D, 0x4D, 0x22, 0x3A, 0x22, 0x42, - 0x53, 0x4D, 0x2D, 0x57, 0x53, 0x33, 0x36, 0x41, 0x2D, 0x48, 0x30, 0x31, 0x2D, 0x31, 0x33, 0x31, 0x31, 0x2D, - 0x30, 0x30, 0x30, 0x30, 0x22, 0x2C, 0x22, 0x4D, 0x53, 0x22, 0x3A, 0x22, 0x30, 0x30, 0x31, 0x42, 0x5A, 0x52, - 0x31, 0x35, 0x32, 0x31, 0x30, 0x37, 0x30, 0x30, 0x31, 0x39, 0x22, 0x2C, 0x22, 0x49, 0x53, 0x22, 0x3A, 0x66, - 0x61, 0x6C, 0x73, 0x65, 0x2C, 0x22, 0x49, 0x54, 0x22, 0x3A, 0x22, 0x55, 0x4E, 0x44, 0x45, 0x46, 0x49, 0x4E, - 0x45, 0x44, 0x22, 0x2C, 0x22, 0x49, 0x44, 0x22, 0x3A, 0x22, 0x22, 0x2C, 0x22, 0x52, 0x44, 0x22, 0x3A, 0x5B, - 0x7B, 0x22, 0x54, 0x4D, 0x22, 0x3A, 0x22, 0x31, 0x39, 0x37, 0x30, 0x2D, 0x30, 0x31, 0x2D, 0x30, 0x31, 0x54, - 0x30, 0x30, 0x3A, 0x30, 0x30, 0x3A, 0x30, 0x30, 0x2C, 0x30, 0x30, 0x30, 0x2B, 0x30, 0x30, 0x30, 0x30, 0x20, - 0x55, 0x22, 0x2C, 0x22, 0x54, 0x58, 0x22, 0x3A, 0x22, 0x43, 0x22, 0x2C, 0x22, 0x52, 0x56, 0x22, 0x3A, 0x31, - 0x39, 0x34, 0x30, 0x2C, 0x22, 0x52, 0x49, 0x22, 0x3A, 0x22, 0x31, 0x2D, 0x30, 0x3A, 0x31, 0x2E, 0x38, 0x2E, - 0x30, 0x2A, 0x31, 0x39, 0x38, 0x22, 0x2C, 0x22, 0x52, 0x55, 0x22, 0x3A, 0x22, 0x57, 0x68, 0x22, 0x2C, 0x22, - 0x58, 0x56, 0x22, 0x3A, 0x39, 0x30, 0x34, 0x30, 0x2C, 0x22, 0x58, 0x49, 0x22, 0x3A, 0x22, 0x31, 0x2D, 0x30, - 0x3A, 0x31, 0x2E, 0x38, 0x2E, 0x30, 0x2A, 0x32, 0x35, 0x35, 0x22, 0x2C, 0x22, 0x58, 0x55, 0x22, 0x3A, 0x22, - 0x57, 0x68, 0x22, 0x2C, 0x22, 0x58, 0x54, 0x22, 0x3A, 0x30, 0x2C, 0x22, 0x52, 0x54, 0x22, 0x3A, 0x22, 0x41, - 0x43, 0x22, 0x2C, 0x22, 0x45, 0x46, 0x22, 0x3A, 0x22, 0x22, 0x2C, 0x22, 0x53, 0x54, 0x22, 0x3A, 0x22, 0x47, - 0x22, 0x7D, 0x5D, 0x7D, 0x7C, 0x7B, 0x22, 0x53, 0x41, 0x22, 0x3A, 0x22, 0x45, 0x43, 0x44, 0x53, 0x41, 0x2D, - 0x73, 0x65, 0x63, 0x70, 0x32, 0x35, 0x36, 0x72, 0x31, 0x2D, 0x53, 0x48, 0x41, 0x32, 0x35, 0x36, 0x22, 0x2C, - 0x22, 0x53, 0x44, 0x22, 0x3A, 0x22, 0x33, 0x30, 0x34, 0x34, 0x30, 0x32, 0x32, 0x30, 0x32, 0x36, 0x65, 0x31, - 0x62, 0x33, 0x30, 0x33, 0x33, 0x33, 0x64, 0x35, 0x37, 0x62, 0x31, 0x35, 0x37, 0x63, 0x33, 0x39, 0x35, 0x39, - 0x34, 0x34, 0x39, 0x64, 0x64, 0x37, 0x66, 0x62, 0x35, 0x36, 0x32, 0x36, 0x30, 0x36, 0x62, 0x34, 0x36, 0x39, - 0x32, 0x32, 0x64, 0x34, 0x63, 0x37, 0x38, 0x34, 0x66, 0x38, 0x62, 0x36, 0x61, 0x65, 0x31, 0x66, 0x64, 0x31, - 0x33, 0x33, 0x63, 0x35, 0x66, 0x36, 0x30, 0x32, 0x32, 0x30, 0x36, 0x30, 0x36, 0x66, 0x62, 0x37, 0x35, 0x30, - 0x61, 0x33, 0x65, 0x38, 0x64, 0x36, 0x33, 0x34, 0x30, 0x39, 0x32, 0x33, 0x37, 0x37, 0x33, 0x33, 0x65, 0x62, - 0x37, 0x62, 0x62, 0x34, 0x64, 0x35, 0x36, 0x34, 0x65, 0x32, 0x38, 0x63, 0x37, 0x31, 0x34, 0x36, 0x33, 0x30, - 0x35, 0x31, 0x34, 0x33, 0x62, 0x64, 0x38, 0x32, 0x38, 0x35, 0x61, 0x66, 0x63, 0x31, 0x62, 0x33, 0x30, 0x64, - 0x31, 0x65, 0x22, 0x7D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00}; - - bsm::SignedOCMFSnapshot signedOCMFSnapshot(data); - - // model_to_stream(std::cout, signedOCMFSnapshot, { 0 ,1,2,3, 4 } ); - - std::string expected_ocmf_string{ - "OCMF|{\"FV\":\"1.0\",\"GI\":\"BAUER Electronic " - "BSM-WS36A-H01-1311-0000\",\"GS\":\"001BZR1521070019\",\"GV\":\"1.9:32CA:AFF4, " - "6d1dd3c\",\"PG\":\"T565\",\"MV\":\"BAUER " - "Electronic\",\"MM\":\"BSM-WS36A-H01-1311-0000\",\"MS\":\"001BZR1521070019\",\"IS\":false,\"IT\":\"UNDEFINED\"," - "\"ID\":\"\",\"RD\":[{\"TM\":\"1970-01-01T00:00:00,000+0000 " - "U\",\"TX\":\"C\",\"RV\":1940,\"RI\":\"1-0:1.8.0*198\",\"RU\":\"Wh\",\"XV\":9040,\"XI\":\"1-0:1.8.0*255\"," - "\"XU\":\"Wh\",\"XT\":0,\"RT\":\"AC\",\"EF\":\"\",\"ST\":\"G\"}]}|{\"SA\":\"ECDSA-secp256r1-SHA256\",\"SD\":" - "\"3044022026e1b30333d57b157c3959449dd7fb562606b46922d4c784f8b6ae1fd133c5f60220606fb750a3e8d63409237733eb7bb4d5" - "64e28c7146305143bd8285afc1b30d1e\"}"}; - - // This should be enough testing here... - EXPECT_EQ(signedOCMFSnapshot.O(), expected_ocmf_string); -} - -TEST(TestModel, SunspecACMeter) { - - // clang-format off - /* - ->bsmtool --trace --verbose --device /dev/ttyUSB0 get ac_meter - -/dev/ttyUSB0:42[addr=40092] ->2A039C9C00696D81 9c9c: 40092 => Sunspec Address 40093, this means the BSM python tool skips ModelID (40091) and Model payload length (40092) -/dev/ttyUSB0:42[addr=40092] <--2A03D2000D000D00000000FFFE8000093C000000008000800080008000FFFF01F4FFFF0002000200000000000100030003000000000001FFFEFFFE000000000001800002AE00000000FFFF00000000000000000000000000000000000022A60000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000092F5 -ac_meter: - ID: 203 - L: 105 - A: 0.13 A - AphA: 0.13 A - AphB: 0.0 A - AphC: 0.0 A - PhV: None - PhVphA: 236.4 V - PhVphB: 0.0 V - PhVphC: 0.0 V - PPV: None - PhVphAB: None - PhVphBC: None - PhVphCA: None - Hz: 50.0 Hz - W: 20.0 W - WphA: 20.0 W - WphB: 0.0 W - WphC: 0.0 W - VA: 30.0 VA - VAphA: 30.0 VA - VAphB: 0.0 VA - VAphC: 0.0 VA - VAR: -20.0 var - VARphA: -20.0 var - VARphB: 0.0 var - VARphC: 0.0 var - PF: None - PFphA: 68.60000000000001 Pct - PFphB: 0.0 Pct - PFphC: 0.0 Pct - TotWhExp: None - TotWhExpPhA: None - TotWhExpPhB: None - TotWhExpPhC: None - TotWhImp: 8870 Wh - TotWhImpPhA: None - TotWhImpPhB: None - TotWhImpPhC: None - TotVAhExp: None - TotVAhExpPhA: None - TotVAhExpPhB: None - TotVAhExpPhC: None - TotVAhImp: None - TotVAhImpPhA: None - TotVAhImpPhB: None - TotVAhImpPhC: None - TotVArhImpQ1: None - TotVArhImpQ1PhA: None - TotVArhImpQ1PhB: None - TotVArhImpQ1PhC: None - TotVArhImpQ2: None - TotVArhImpQ2PhA: None - TotVArhImpQ2PhB: None - TotVArhImpQ2PhC: None - TotVArhExpQ3: None - TotVArhExpQ3PhA: None - TotVArhExpQ3PhB: None - TotVArhExpQ3PhC: None - TotVArhExpQ4: None - TotVArhExpQ4PhA: None - TotVArhExpQ4PhB: None - TotVArhExpQ4PhC: None - Evt: 0 - - - The original data obtained from the powermeter have been changed, since the bsm python tool starts reading at sunspec address 40093, - but SunspecModel::ACMeter expects the data to start at sunspec address 40091. - The first four bytes have been added manually and are the default values for this sunspec model. - */ - - // clang-format on - - transport::DataVector data = {0x00, 203, 0x00, 105, 0x00, 0x0D, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, - 0x80, 0x00, 0x09, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, - 0x80, 0x00, 0xFF, 0xFF, 0x01, 0xF4, 0xFF, 0xFF, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0xFF, 0xFE, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x02, 0xAE, - 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xA6, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, - 0x00, 0x00, 0x00, 0x00}; // , 0x92, 0xF5 }; - - sunspec_model::ACMeter ac_meter(data); - - std::cout << "ID : " << ac_meter.ID() << std::endl; - std::cout << "A : " << ac_meter.A() << std::endl; - - // model_to_stream(std::cout, ac_meter, { 0, 1 ,2 ,3 , 4 ,5 , 6 , 8 , 36 } ); - - EXPECT_FLOAT_EQ(ac_meter.A() * pow(10, ac_meter.A_SF()), 0.13); // check Amps and its scaling factor - - EXPECT_FLOAT_EQ(ac_meter.PhVphA() * pow(10, ac_meter.PF_SF()), 236.4); // check PhVphA and scaling factor - - EXPECT_FLOAT_EQ(ac_meter.Hz() * pow(10, ac_meter.Hz_SF()), 50.0); // check Hz and scaling factor - - // same stuff for the other suspects. - - EXPECT_FLOAT_EQ(ac_meter.W() * pow(10, ac_meter.W_SF()), 20.0); - - EXPECT_FLOAT_EQ(ac_meter.VA() * pow(10, ac_meter.VA_SF()), 30.0); - - EXPECT_FLOAT_EQ(ac_meter.VAphA() * pow(10, ac_meter.VA_SF()), 30.0); - - EXPECT_FLOAT_EQ(ac_meter.VAR() * pow(10, ac_meter.VAR_SF()), -20.0); - - EXPECT_FLOAT_EQ(ac_meter.VARphA() * pow(10, ac_meter.VAR_SF()), -20.0); - - EXPECT_FLOAT_EQ(ac_meter.PFphA() * pow(10, ac_meter.PF_SF()), 68.60000000000001); -} - -template -std::ostream& test_model_to_stream(std::ostream& out, const MODEL& model, - std::initializer_list index_list) { - - for (std::size_t model_index : index_list) { - out << "Id : " << MODEL::Model[model_index].id << " offset " << MODEL::Model[model_index].offset << " " - << " value " << point_value_to_string(model.m_data, MODEL::Model[model_index]) << "\n"; - } - - return out; -} - -template bool invalid_value(T v) { - - if constexpr (PT == PointType::acc32) { - return invalid_point_value::acc32 == v; - } - - if constexpr (PT == PointType::bitfield32) { - return invalid_point_value::bitfield32 == v; - } - - if constexpr (PT == PointType::enum16) { - return invalid_point_value::enum16 == v; - } - - if constexpr (PT == PointType::int16) { - return invalid_point_value::int16 == v; - } - - if constexpr (PT == PointType::sunssf) { - return invalid_point_value::sunssf == v; - } - - if constexpr (PT == PointType::uint16) { - return invalid_point_value::uint16 == v; - } - - if constexpr (PT == PointType::uint32) { - return invalid_point_value::uint32 == v; - } -} - -TEST(TestInvalidValues, ValueCheck) { - - transport::DataVector data = {0x00, 203, 0x00, 105, 0x00, 0x0D, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFE, - 0x80, 0x00, 0x09, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, - 0x80, 0x00, 0xFF, 0xFF, 0x01, 0xF4, 0xFF, 0xFF, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0xFF, 0xFE, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x00, 0x02, 0xAE, - 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x22, 0xA6, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, - 0x00, 0x00, 0x00, 0x00}; // , 0x92, 0xF5 }; - - sunspec_model::ACMeter ac_meter(data); - - test_model_to_stream(std::cout, ac_meter, {9}); - - std::cout << "is invalid: " << std::boolalpha << invalid_value(1) << std::endl; - - std::cout << "is invalid: " << std::boolalpha << invalid_value(invalid_point_value::acc32) - << std::endl; -} diff --git a/modules/PyEvJosev/utilities.py b/modules/PyEvJosev/utilities.py index e0c57d30f..1a88720e9 100644 --- a/modules/PyEvJosev/utilities.py +++ b/modules/PyEvJosev/utilities.py @@ -27,6 +27,7 @@ def emit(self, record): else: log.debug(msg) + def setup_everest_logging(): # remove all logging handler so that we'll have only our custom one # FIXME (aw): this is probably bad practice because if everyone does that, only the last one might survive @@ -40,6 +41,7 @@ def setup_everest_logging(): logging.getLogger().addHandler(handler) + def choose_first_ipv6_local() -> str: for iface in netifaces.interfaces(): if netifaces.AF_INET6 in netifaces.ifaddresses(iface): @@ -50,14 +52,17 @@ def choose_first_ipv6_local() -> str: log.warning('No necessary IPv6 link-local address was found!') return 'eth0' + def determine_network_interface(preferred_interface: str) -> str: if preferred_interface == "auto": return choose_first_ipv6_local() elif preferred_interface not in netifaces.interfaces(): - log.warning(f"The network interface {preferred_interface} was not found!") + log.warning( + f"The network interface {preferred_interface} was not found!") return preferred_interface + def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None: josev_config.use_tls = everest_config['tls_active'] @@ -93,5 +98,5 @@ def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None: josev_config.supported_protocols = load_requested_protocols(protocols) josev_config.supported_energy_services = load_requested_energy_services( - ['DC'] + ['DC','DC_BPT'] ) diff --git a/modules/RsIskraMeter/BUILD.bazel b/modules/RsIskraMeter/BUILD.bazel index 5de28bccd..ef47b9613 100644 --- a/modules/RsIskraMeter/BUILD.bazel +++ b/modules/RsIskraMeter/BUILD.bazel @@ -11,6 +11,7 @@ cargo_build_script( }, data = [ "manifest.yaml", + "@everest-core//errors", "@everest-core//interfaces", "@everest-core//types", ], diff --git a/modules/RsIskraMeter/Cargo.toml b/modules/RsIskraMeter/Cargo.toml index b04b84684..d0f94f9e0 100644 --- a/modules/RsIskraMeter/Cargo.toml +++ b/modules/RsIskraMeter/Cargo.toml @@ -8,6 +8,7 @@ everestrs-build = { workspace=true } [dependencies] anyhow = "1.0.75" +backon = "1.2.0" chrono = "0.4.31" everestrs = { workspace=true } log = "0.4.20" diff --git a/modules/RsIskraMeter/manifest.yaml b/modules/RsIskraMeter/manifest.yaml index 81dcd8ff9..6b820df84 100644 --- a/modules/RsIskraMeter/manifest.yaml +++ b/modules/RsIskraMeter/manifest.yaml @@ -32,6 +32,10 @@ config: description: Identification information for the charge point. default: "" type: string + communication_errors_threshold: + description: The maximum number of consecutive errors allowed before a persistent error is reported + default: 10 + type: integer provides: meter: description: Implementation of the driver functionality diff --git a/modules/RsIskraMeter/src/main.rs b/modules/RsIskraMeter/src/main.rs index fa6a35075..48a162366 100644 --- a/modules/RsIskraMeter/src/main.rs +++ b/modules/RsIskraMeter/src/main.rs @@ -41,9 +41,12 @@ include!(concat!(env!("OUT_DIR"), "/generated.rs")); mod utils; use anyhow::Result; +use backon::BlockingRetryable; +use backon::ConstantBuilder; use chrono::{Local, Offset, Utc}; use everestrs::serde as everest_serde; use everestrs::serde_json as everest_serde_json; +use generated::errors::powermeter::{Error, PowermeterError}; use generated::types::powermeter::{ Powermeter, TransactionRequestStatus, TransactionStartResponse, TransactionStopResponse, }; @@ -821,6 +824,7 @@ enum StateMachine { /// Main class implementing all EVerest traits. struct IskraMeter { state_machine: Mutex, + communication_errors_threshold: usize, } impl generated::OnReadySubscriber for IskraMeter { @@ -850,19 +854,36 @@ impl generated::OnReadySubscriber for IskraMeter { let ready_state_clone = ready_state.clone(); let power_meter_clone = publishers.meter.clone(); + + let backoff = ConstantBuilder::default() + .with_delay(std::time::Duration::from_secs(10)) + .with_max_times(self.communication_errors_threshold); + std::thread::spawn(move || loop { std::thread::sleep(std::time::Duration::from_secs(5)); - let res = ready_state_clone.read_meter_value(); - match res { + + match (|| ready_state_clone.read_meter_value()) + .retry(backoff) + .notify(|err: &anyhow::Error, dur: std::time::Duration| { + log::warn!("retrying {:?} after {:?}", err, dur); + }) + .call() + { Ok(meter) => { - log::info!("Got meter value {:?}", meter); + log::debug!("Got meter value {:?}", meter); match power_meter_clone.powermeter(meter) { - Ok(_) => log::info!("Successfully published meter value"), + Ok(_) => log::debug!("Successfully published meter value"), Err(e) => log::error!("Failed to post meter values {:?}", e), } + power_meter_clone + .clear_error(Error::Powermeter(PowermeterError::CommunicationFault)); } - Err(e) => log::error!("Failed to read meter value {:?}", e), - } + Err(e) => { + log::error!("Failed to read meter value {:?}", e); + power_meter_clone + .raise_error(Error::Powermeter(PowermeterError::CommunicationFault)); + } + }; }); // Finally update the state in the lock. @@ -931,6 +952,7 @@ fn main() { config.powermeter_device_id, (&config).into(), ))), + communication_errors_threshold: config.communication_errors_threshold as usize, }); let _module = Module::new(class.clone(), class.clone(), class.clone()); diff --git a/modules/RsPaymentTerminal/BUILD.bazel b/modules/RsPaymentTerminal/BUILD.bazel index 596ee399e..aa8adc2c6 100644 --- a/modules/RsPaymentTerminal/BUILD.bazel +++ b/modules/RsPaymentTerminal/BUILD.bazel @@ -11,6 +11,7 @@ cargo_build_script( }, data = [ "manifest.yaml", + "@everest-core//errors", "@everest-core//interfaces", "@everest-core//types", ], 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/modules/module.bzl b/modules/module.bzl index dd8c8679d..42250ec25 100644 --- a/modules/module.bzl +++ b/modules/module.bzl @@ -76,6 +76,14 @@ def cc_everest_module( "generated/modules/" + name, ], visibility = ["//visibility:public"], + # See https://github.com/HowardHinnant/date/issues/324 + local_defines = [ + "BUILD_TZ_LIB=ON", + "USE_SYSTEM_TZ_DB=ON", + "USE_OS_TZDB=1", + "USE_AUTOLOAD=0", + "HAS_REMOTE_API=0", + ], ) native.genrule( diff --git a/modules/rust_examples/RsExample/Cargo.toml b/modules/rust_examples/RsExample/Cargo.toml index 7a708d04e..b010ae2a6 100644 --- a/modules/rust_examples/RsExample/Cargo.toml +++ b/modules/rust_examples/RsExample/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] everestrs = { workspace = true } -serde = { version = "1.0.175", features = ["derive"] } +serde = { version = "1.0.200", features = ["derive"] } serde_json = "1" [build-dependencies] diff --git a/modules/rust_examples/RsExampleUser/Cargo.toml b/modules/rust_examples/RsExampleUser/Cargo.toml index d0ca215eb..7b6a1312c 100644 --- a/modules/rust_examples/RsExampleUser/Cargo.toml +++ b/modules/rust_examples/RsExampleUser/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] everestrs = { workspace = true } -serde = { version = "1.0.175", features = ["derive"] } +serde = { version = "1.0.200", features = ["derive"] } serde_json = "1" [build-dependencies] diff --git a/modules/simulation/JsYetiSimulator/index.js b/modules/simulation/JsYetiSimulator/index.js index 5bf3dcdc2..2d5c9ff90 100644 --- a/modules/simulation/JsYetiSimulator/index.js +++ b/modules/simulation/JsYetiSimulator/index.js @@ -912,6 +912,12 @@ function power_meter_external(p) { L2: p.freqL2, L3: p.freqL3, }, + temperatures: [ + { + temperature: p.tempL1, + location: "Body" + } + ] }); } diff --git a/modules/simulation/SlacSimulator/CMakeLists.txt b/modules/simulation/SlacSimulator/CMakeLists.txt index 7a884c9d5..f60f20c30 100644 --- a/modules/simulation/SlacSimulator/CMakeLists.txt +++ b/modules/simulation/SlacSimulator/CMakeLists.txt @@ -9,6 +9,11 @@ ev_setup_cpp_module() # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 # insert your custom targets and additional config variables here +target_compile_options(${MODULE_NAME} + PRIVATE + -Wimplicit-fallthrough + -Werror=switch-enum +) # ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 target_sources(${MODULE_NAME} diff --git a/modules/simulation/SlacSimulator/ev/ev_slacImpl.hpp b/modules/simulation/SlacSimulator/ev/ev_slacImpl.hpp index a7569576f..70851e76a 100644 --- a/modules/simulation/SlacSimulator/ev/ev_slacImpl.hpp +++ b/modules/simulation/SlacSimulator/ev/ev_slacImpl.hpp @@ -52,7 +52,7 @@ class ev_slacImpl : public ev_slacImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 // insert your private definitions here - util::State state; + util::State state{util::State::UNMATCHED}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/simulation/SlacSimulator/evse/slacImpl.hpp b/modules/simulation/SlacSimulator/evse/slacImpl.hpp index 1de2d0116..bc8056dae 100644 --- a/modules/simulation/SlacSimulator/evse/slacImpl.hpp +++ b/modules/simulation/SlacSimulator/evse/slacImpl.hpp @@ -59,7 +59,7 @@ class slacImpl : public slacImplBase { void set_state_to_unmatched(); void set_state_to_matching(); - util::State state; + util::State state{util::State::UNMATCHED}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/simulation/SlacSimulator/util/state.cpp b/modules/simulation/SlacSimulator/util/state.cpp index 31299521e..b22167350 100644 --- a/modules/simulation/SlacSimulator/util/state.cpp +++ b/modules/simulation/SlacSimulator/util/state.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include "state.hpp" +#include #include namespace module::util { @@ -13,9 +14,8 @@ std::string state_to_string(State state) { return "MATCHING"; case State::MATCHED: return "MATCHED"; - default: - return ""; } + throw std::out_of_range("Could not convert State to string"); } } // namespace module::util diff --git a/tests/conftest.py b/tests/conftest.py index a37917370..a1e66aa6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,3 +27,9 @@ def pytest_configure(config): continue pytest.everest_configs['params'].append(config_path) pytest.everest_configs['ids'].append(config_id) + +@pytest.fixture +def started_test_controller(test_controller): + test_controller.start() + yield test_controller + test_controller.stop() diff --git a/tests/everest-core_tests/config/config-test-cpp-error-handling.yaml b/tests/everest-core_tests/config/config-test-cpp-error-handling.yaml index 0d398efbe..a2351b557 100644 --- a/tests/everest-core_tests/config/config-test-cpp-error-handling.yaml +++ b/tests/everest-core_tests/config/config-test-cpp-error-handling.yaml @@ -11,12 +11,3 @@ active_modules: error_raiser: - module_id: probe implementation_id: main - probe: - module: ProbeModule - connections: - test_error_handling: - - module_id: test_error_handling - implementation_id: main - test_error_handling_not_req: - - module_id: test_error_handling_not_req - implementation_id: main diff --git a/tests/framework_tests/cpp_tests.py b/tests/framework_tests/cpp_tests.py index c607f056e..641c5453e 100644 --- a/tests/framework_tests/cpp_tests.py +++ b/tests/framework_tests/cpp_tests.py @@ -6,6 +6,7 @@ import asyncio from everest.framework import error +from everest.testing.core_utils.common import Requirement from everest.testing.core_utils.fixtures import * from everest.testing.core_utils.everest_core import EverestCore from everest.testing.core_utils.probe_module import ProbeModule @@ -436,12 +437,23 @@ def get_state(self) -> ErrorHandlingTesterState: len_test_error_handling_errors_cleared_global_all=len(self.test_error_handling['errors_cleared_global_all']), ) return state + +@pytest.mark.probe_module( + connections={ + "test_error_handling": [ + Requirement(module_id="test_error_handling", implementation_id="main") + ], + "test_error_handling_not_req": [ + Requirement(module_id="test_error_handling_not_req", implementation_id="main") + ] + } +) class TestErrorHandling: """ Tests for error handling """ @pytest.fixture - def probe_module(self, everest_core: EverestCore): + def probe_module(self, started_test_controller, everest_core): return ProbeModule(everest_core.get_runtime_session()) @pytest.fixture @@ -455,15 +467,13 @@ def error_handling_tester(self, probe_module: ProbeModule): @pytest.mark.asyncio async def test_raise_error( self, - everest_core: EverestCore, - error_handling_tester: ErrorHandlingTester + error_handling_tester: ErrorHandlingTester, ): """ Tests that errors are raised correctly. The probe module triggers the TestErrorHandling module to raise an error, and then checks that the error is raised by subscribing to it. """ - everest_core.start(standalone_module='probe') err_args = await error_handling_tester.test_error_handling_raise_error_a() await asyncio.sleep(0.5) expected_state = ErrorHandlingTesterState( @@ -481,7 +491,6 @@ async def test_raise_error( @pytest.mark.asyncio async def test_clear_errors_by_type( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -491,7 +500,6 @@ async def test_clear_errors_by_type( and then checks that the correct errors are cleared. """ - everest_core.start(standalone_module='probe') await error_handling_tester.test_error_handling_raise_error_a() await error_handling_tester.test_error_handling_raise_error_b() await error_handling_tester.test_error_handling_raise_error_c() @@ -521,7 +529,6 @@ async def test_clear_errors_by_type( @pytest.mark.asyncio async def test_clear_all_errors( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -531,7 +538,6 @@ async def test_clear_all_errors( and then checks that all errors are cleared. """ - everest_core.start(standalone_module='probe') await error_handling_tester.test_error_handling_raise_error_a() await error_handling_tester.test_error_handling_raise_error_b() await error_handling_tester.test_error_handling_raise_error_c() @@ -570,7 +576,6 @@ async def test_clear_all_errors( @pytest.mark.asyncio async def test_receive_req_error( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -579,7 +584,6 @@ async def test_receive_req_error( Checks that the error is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_object = error_handling_tester.probe_module_main_raise_error_a() await asyncio.sleep(0.5) expected_state = ErrorHandlingTesterState( @@ -599,7 +603,6 @@ async def test_receive_req_error( @pytest.mark.asyncio async def test_receive_req_error_cleared( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -609,7 +612,6 @@ async def test_receive_req_error_cleared( Checks that the error_cleared is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_object = error_handling_tester.probe_module_main_raise_error_a() await asyncio.sleep(0.5) error_handling_tester.probe_module_main_clear_error(err_object.type, err_object.sub_type) @@ -633,7 +635,6 @@ async def test_receive_req_error_cleared( @pytest.mark.asyncio async def test_receive_req_not_sub_error( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -642,7 +643,6 @@ async def test_receive_req_not_sub_error( Checks that the error is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_object = error_handling_tester.probe_module_main_raise_error_c() await asyncio.sleep(0.5) expected_state = ErrorHandlingTesterState( @@ -660,7 +660,6 @@ async def test_receive_req_not_sub_error( @pytest.mark.asyncio async def test_receive_req_not_sub_error_cleared( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -670,7 +669,6 @@ async def test_receive_req_not_sub_error_cleared( Checks that the error_cleared is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_object = error_handling_tester.probe_module_main_raise_error_c() await asyncio.sleep(0.5) error_handling_tester.probe_module_main_clear_error(err_object.type, err_object.sub_type) @@ -692,7 +690,6 @@ async def test_receive_req_not_sub_error_cleared( @pytest.mark.asyncio async def test_receive_not_req_error( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -702,7 +699,6 @@ async def test_receive_not_req_error( Checks that the error is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_arg = await error_handling_tester.test_error_handling_not_req_raise_error_a() await asyncio.sleep(0.5) expected_state = ErrorHandlingTesterState( @@ -718,7 +714,6 @@ async def test_receive_not_req_error( @pytest.mark.asyncio async def test_receive_not_req_error_cleared( self, - everest_core: EverestCore, error_handling_tester: ErrorHandlingTester ): """ @@ -730,7 +725,6 @@ async def test_receive_not_req_error_cleared( Checks that the error is subscribed correctly. """ - everest_core.start(standalone_module='probe') err_arg = await error_handling_tester.test_error_handling_not_req_raise_error_a() await asyncio.sleep(0.5) await error_handling_tester.test_error_handling_not_req_clear_error(err_arg['type'], err_arg['sub_type']) diff --git a/tests/include/everest/logging.hpp b/tests/include/everest/logging.hpp new file mode 100644 index 000000000..c245fde54 --- /dev/null +++ b/tests/include/everest/logging.hpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include + +namespace { +class UTestLogger { +private: + std::ostream& m_stream; + +public: + UTestLogger() = delete; + UTestLogger(const char* file, int line) : UTestLogger(std::cerr, "Error: ", file, line) { + } + UTestLogger(std::ostream& stream, const char* level, const char* file, int line) : m_stream(stream) { + const auto f = std::filesystem::path((file == nullptr) ? "(unknown)" : file); + m_stream << level << f.filename().string() << ':' << line << ' '; + } + ~UTestLogger() { + m_stream << std::endl; + } + template constexpr std::ostream& operator<<(const T& item) { + return m_stream << item; + } +}; + +#define EVLOG_critical UTestLogger(std::cerr, "Critical: ", __FILE__, __LINE__) +#define EVLOG_error UTestLogger(std::cerr, "Error: ", __FILE__, __LINE__) +#define EVLOG_warning UTestLogger(std::cout, "Warning: ", __FILE__, __LINE__) +#define EVLOG_info UTestLogger(std::cout, "Info: ", __FILE__, __LINE__) +#define EVLOG_debug UTestLogger(std::cout, "Debug: ", __FILE__, __LINE__) +#define EVLOG_AND_THROW(ex) \ + do { \ + try { \ + throw ex; \ + } catch (std::exception & e) { \ + EVLOG_error << e.what(); \ + throw; \ + } \ + } while (0) +} // namespace 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..53bce2875 --- /dev/null +++ b/tests/ocpp_tests/conftest.py @@ -0,0 +1,462 @@ +# 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", + "get_verify_location", + 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-costandprice.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp16-costandprice.yaml new file mode 100644 index 000000000..d3ff8a54f --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp16-costandprice.yaml @@ -0,0 +1,122 @@ +active_modules: + evse_manager: + 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 + ev_manager: + module: EvManager + 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 + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: FindFirst + connections: + token_provider: + - module_id: token_provider + implementation_id: main + - module_id: ocpp + implementation_id: auth_provider + token_validator: + - module_id: ocpp + implementation_id: auth_validator + evse_manager: + - module_id: evse_manager + implementation_id: evse + ocpp: + module: OCPP + config_module: + ChargePointConfigPath: libocpp-config-costandprice.json + connections: + evse_manager: + - module_id: evse_manager + 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 + display_message: + - module_id: display_message + implementation_id: display_message + display_message: + module: TerminalCostAndPriceMessage + connections: + session_cost: + - module_id: ocpp + implementation_id: session_cost + evse_security: + module: EvseSecurity + config_module: + private_key_password: "123456" + token_provider: + 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: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter + api: + module: API + connections: + evse_manager: + - module_id: evse_manager + implementation_id: evse + ocpp: + - module_id: ocpp + implementation_id: ocpp_generic + error_history: + - module_id: error_history + implementation_id: error_history + error_history: + module: ErrorHistory + config_implementation: + error_history: + database_path: /tmp/error_history.db + 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-costandprice.yaml b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-costandprice.yaml new file mode 100644 index 000000000..77b3ab771 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201-costandprice.yaml @@ -0,0 +1,162 @@ +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 + 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 + 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 + display_message: + - module_id: display_message + implementation_id: display_message + persistent_store: + module: PersistentStore + config_module: + sqlite_db_file_path: persistent_store.db + display_message: + module: TerminalCostAndPriceMessage + connections: + session_cost: + - module_id: ocpp + implementation_id: session_cost + 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" + 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 + ev_manager_1: + module: EvManager + 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_manager_2: + module: EvManager + config_module: + connector_id: 2 + 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_2 + implementation_id: ev_board_support + error_history: + module: ErrorHistory + config_implementation: + error_history: + database_path: /tmp/error_history.db + system: + module: System + +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..621b028fb --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/everest-config-ocpp201.yaml @@ -0,0 +1,192 @@ +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 + reservation: + - module_id: auth + implementation_id: reservation + 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-costandprice.json b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-costandprice.json new file mode 100644 index 000000000..019dff473 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest-aux/config/libocpp-config-costandprice.json @@ -0,0 +1,93 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:9000/cp001", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "LogMessages": true + }, + "Core": { + "AuthorizeRemoteTxRequests": 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,SoC", + "MeterValueSampleInterval": 60, + "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,CostAndPrice", + "TransactionMessageAttempts": 5, + "TransactionMessageRetryInterval": 1, + "UnlockConnectorOnEVSideDisconnect": true + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Security": { + "CpoName": "Pionix", + "AuthorizationKey": "AABBCCDDEEFFGGHH", + "SecurityProfile": 0, + "AdditionalRootCertificateCheck": true + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 42, + "SendLocalListMaxLength": 42 + }, + "CostAndPrice": { + "CustomDisplayCostAndPrice": true, + "NumberOfDecimalsForCostValues": 4, + "DefaultPrice": + { + "priceText": "This is the price", + "priceTextOffline": "Show this price text when offline!", + "chargingPrice": + { + "kWhPrice": 3.14, + "hourPrice": 0.42 + } + }, + "DefaultPriceText": + { + "priceTexts": + [ + { + "priceText": "This is the price", + "priceTextOffline": "Show this price text when offline!", + "language": "en" + }, + { + "priceText": "Dit is de prijs", + "priceTextOffline": "Laat dit zien wanneer de charging station offline is!", + "language": "nl" + }, + { + "priceText": "Dette er prisen", + "priceTextOffline": "Vis denne pristeksten når du er frakoblet", + "language": "nb_NO" + } + ] + }, + "TimeOffset": "00:00", + "NextTimeOffsetTransitionDateTime": "2024-01-01T00:00:00", + "TimeOffsetNextTransition": "01:00", + "CustomIdleFeeAfterStop": false, + "SupportedLanguages": "en, nl, de, nb_NO", + "CustomMultiLanguageMessages": true, + "Language": "en" + }, + "Reservation": { + "ReserveConnectorZeroSupported": true + } +} 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..1cdefb848 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest_test_utils.py @@ -0,0 +1,642 @@ +# 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", + "reservations", + ]: + 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/everest_test_utils_probe_modules.py b/tests/ocpp_tests/test_sets/everest_test_utils_probe_modules.py new file mode 100644 index 000000000..bdb265bb6 --- /dev/null +++ b/tests/ocpp_tests/test_sets/everest_test_utils_probe_modules.py @@ -0,0 +1,75 @@ +import pytest +import pytest_asyncio + +from copy import deepcopy +from typing import Dict, List + +from everest.testing.ocpp_utils.central_system import CentralSystem +from everest.testing.core_utils.probe_module import ProbeModule +from everest.testing.core_utils import EverestConfigAdjustmentStrategy + + +@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 ChargePoint201. Requires central_system_v201 + """ + # wait for libocpp to go online + cp = await central_system.wait_for_chargepoint() + yield cp + await cp.stop() + + +class ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment(EverestConfigAdjustmentStrategy): + """ + Probe module to be able to 'inject' metervalues + """ + def __init__(self, evse_manager_ids: List[str]): + self.evse_manager_ids = evse_manager_ids + + def adjust_everest_configuration(self, everest_config: Dict): + adjusted_config = deepcopy(everest_config) + + adjusted_config["active_modules"]["grid_connection_point"]["connections"]["powermeter"] = [ + {"module_id": "probe", "implementation_id": "ProbeModulePowerMeter"}] + + for evse_manager_id in self.evse_manager_ids: + adjusted_config["active_modules"][evse_manager_id]["connections"]["powermeter_grid_side"] = [ + {"module_id": "probe", "implementation_id": "ProbeModulePowerMeter"}] + + return adjusted_config + + +class ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment(EverestConfigAdjustmentStrategy): + """ + Probe module to be able to mock display messages + """ + + def adjust_everest_configuration(self, everest_config: Dict): + adjusted_config = deepcopy(everest_config) + + adjusted_config["active_modules"]["ocpp"]["connections"]["display_message"] = [ + {"module_id": "probe", "implementation_id": "ProbeModuleDisplayMessage"}] + + return adjusted_config + + +class ProbeModuleCostAndPriceSessionCostConfigurationAdjustment(EverestConfigAdjustmentStrategy): + """ + Probe module to be able to mock the session cost interface calls + """ + + def adjust_everest_configuration(self, everest_config: Dict): + adjusted_config = deepcopy(everest_config) + + adjusted_config["active_modules"]["probe"]["connections"]["session_cost"] = [ + {"module_id": "ocpp", "implementation_id": "session_cost"}] + + return adjusted_config 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/california_pricing_ocpp16.py b/tests/ocpp_tests/test_sets/ocpp16/california_pricing_ocpp16.py new file mode 100644 index 000000000..70436afd3 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/california_pricing_ocpp16.py @@ -0,0 +1,953 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +import asyncio +import logging +import json + +from datetime import datetime, timedelta, timezone + +from unittest.mock import Mock, ANY + + +# fmt: off + +from validations import ( + validate_standard_start_transaction, + validate_standard_stop_transaction +) + +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.controller.test_controller_interface import TestController + +from everest_test_utils import * + +from everest_test_utils_probe_modules import (probe_module, + ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment, + ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment, + ProbeModuleCostAndPriceSessionCostConfigurationAdjustment) + +# fmt: on + +from ocpp.v16.enums import * +from ocpp.v16 import call, call_result + + +@pytest.mark.asyncio +@pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) +class TestOcpp16CostAndPrice: + """ + Tests for OCPP 1.6 California Pricing Requirements + """ + + # Running cost request data, to be used in tests + running_cost_data = { + "transactionId": 1, + "timestamp": datetime.now(timezone.utc).isoformat(), "meterValue": 1234000, + "cost": 1.345, + "state": "Charging", + "chargingPrice": { + "kWhPrice": 0.123, "hourPrice": 2.00, "flatFee": 42.42}, + "idlePrice": {"graceMinutes": 30, "hourPrice": 1.00}, + "nextPeriod": { + "atTime": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(), + "chargingPrice": { + "kWhPrice": 0.100, "hourPrice": 4.00, "flatFee": 84.84}, + "idlePrice": {"hourPrice": 0.50} + }, + "triggerMeterValue": { + "atTime": datetime.now(timezone.utc).isoformat(), + "atEnergykWh": 5.0, + "atPowerkW": 8.0, + "atCPStatus": [ChargePointStatus.finishing, ChargePointStatus.suspended_ev] + } + } + + # Final cost request data, to be used in tests. + final_cost_data = { + "transactionId": 1, + "cost": 3.31, + "priceText": "GBP 2.81 @ 0.12/kWh, GBP 0.50 @ 1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: GBP 3.31. Visit www.cpo.com/invoices/13546 for an invoice of your session.", + "priceTextExtra": [ + {"format": "UTF8", "language": "nl", "content": "€2.81 @ €0.12/kWh, €0.50 @ €1/h, TOTAL KWH: 23.4 " + "TIME: 03.50 COST: €3.31. Bezoek www.cpo.com/invoices/13546 " + "voor een factuur van uw laadsessie."}, + {"format": "UTF8", "language": "de", "content": "€2,81 @ €0,12/kWh, €0,50 @ €1/h, GESAMT-KWH: 23,4 " + "ZEIT: 03:50 KOSTEN: €3,31. Besuchen Sie " + "www.cpo.com/invoices/13546 um eine Rechnung für Ihren " + "Ladevorgang zu erhalten."}], + "qrCodeText": "https://www.cpo.com/invoices/13546" + } + + @staticmethod + async def start_transaction(test_controller: TestController, test_utility: TestUtility, charge_point: ChargePoint16, + test_config: OcppTestConfiguration): + """ + Function to start a transaction during tests. + """ + # Start transaction + await charge_point.change_configuration_req(key="MeterValueSampleInterval", value="300") + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status preparing + assert await wait_for_and_validate(test_utility, charge_point, "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, + "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, "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, "StatusNotification", + call.StatusNotificationPayload(1, ChargePointErrorCode.no_error, + ChargePointStatus.charging)) + + test_utility.messages.clear() + + @staticmethod + async def await_mock_called(mock): + while not mock.call_count: + await asyncio.sleep(0.1) + + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_cost_and_price_set_user_price_no_transaction(self, test_config: OcppTestConfiguration, + test_utility: TestUtility, + test_controller: TestController, probe_module, + central_system: CentralSystem): + """ + Test if the datatransfer call returns 'rejected' when session cost is sent while there is no transaction + running. + """ + + logging.info("######### test_cost_and_price_set_user_price_no_transaction #########") + + data = { + "idToken": test_config.authorization_info.valid_id_tag_1, + "priceText": "GBP 0.12/kWh, no idle fee", + "priceTextExtra": [{"format": "UTF8", "language": "nl", + "content": "€0.12/kWh, geen idle fee"}, + {"format": "UTF8", "language": "de", + "content": "€0,12/kWh, keine Leerlaufgebühr"} + ] + } + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + 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() + + await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="SetUserPrice", + data=json.dumps(data)) + + # No session running, datatransfer should return 'accepted' and id token is added to the message + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted), timeout=5) + + # Display message should have received a message with the current price information + data_received = { + 'request': [{'identifier_id': test_config.authorization_info.valid_id_tag_1, 'identifier_type': 'IdToken', + 'message': {'content': 'GBP 0.12/kWh, no idle fee', 'language': 'en'}}, + {'identifier_id': test_config.authorization_info.valid_id_tag_1, 'identifier_type': 'IdToken', + 'message': {'content': '€0.12/kWh, geen idle fee', 'format': 'UTF8', 'language': 'nl'}}, + {'identifier_id': test_config.authorization_info.valid_id_tag_1, 'identifier_type': 'IdToken', + 'message': {'content': '€0,12/kWh, keine Leerlaufgebühr', 'format': 'UTF8', 'language': 'de'}}] + } + + probe_module_mock_fn.assert_called_once_with(data_received) + + @pytest.mark.asyncio + async def test_cost_and_price_set_user_price_no_transaction_no_id_token(self, test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility): + """ + Test if the datatransfer call returns 'rejected' when session cost is sent while there is no transaction + running. + """ + + logging.info("######### test_cost_and_price_set_user_price_no_transaction_no_id_token #########") + + data = { + "priceText": "GBP 0.12/kWh, no idle fee", + "priceTextExtra": [{"format": "UTF8", "language": "nl", + "content": "€0.12/kWh, geen idle fee"}, + {"format": "UTF8", "language": "de", + "content": "€0,12/kWh, keine Leerlaufgebühr"} + ] + } + + await charge_point_v16.data_transfer_req(vendor_id="org.openchargealliance.costmsg", message_id="SetUserPrice", + data=json.dumps(data)) + + # No session running, and no id token, datatransfer should return 'rejected' + assert await wait_for_and_validate(test_utility, charge_point_v16, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), timeout=5) + + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_cost_and_price_set_user_price_with_transaction(self, test_config: OcppTestConfiguration, + test_utility: TestUtility, + test_controller: TestController, probe_module, + central_system: CentralSystem): + """ + Test if user price is sent correctly when there is a transaction. + """ + + logging.info("######### test_cost_and_price_set_user_price_with_transaction #########") + + data = { + "idToken": test_config.authorization_info.valid_id_tag_1, + "priceText": "GBP 0.12/kWh, no idle fee", + "priceTextExtra": [{"format": "UTF8", "language": "nl", + "content": "€0.12/kWh, geen idle fee"}, + {"format": "UTF8", "language": "de", + "content": "€0,12/kWh, keine Leerlaufgebühr"} + ] + } + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + # Send 'set user price', which is tight to a transaction. + await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="SetUserPrice", + data=json.dumps(data)) + + # Datatransfer should be successful. + success = await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted), timeout=5) + + # Display message should have received a message with the current price information + data_received = { + 'request': [{'identifier_id': ANY, 'identifier_type': 'SessionId', + 'message': {'content': 'GBP 0.12/kWh, no idle fee', 'language': 'en'}}, + {'identifier_id': ANY, 'identifier_type': 'SessionId', + 'message': {'content': '€0.12/kWh, geen idle fee', 'format': 'UTF8', 'language': 'nl'}}, + {'identifier_id': ANY, 'identifier_type': 'SessionId', + 'message': {'content': '€0,12/kWh, keine Leerlaufgebühr', 'format': 'UTF8', 'language': 'de'}}] + } + + probe_module_mock_fn.assert_called_once_with(data_received) + + assert success + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_final_cost_no_transaction(self, test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility): + """ + Test sending of final price when there is no transaction: DataTransfer should return rejected. + """ + logging.info("######### test_cost_and_price_final_cost_no_transaction #########") + + await charge_point_v16.data_transfer_req(vendor_id="org.openchargealliance.costmsg", message_id="FinalCost", + data=json.dumps(self.final_cost_data)) + + # Since there is no transaction, datatransfer should return 'rejected' here. + success = await wait_for_and_validate(test_utility, charge_point_v16, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), timeout=5) + assert success + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_final_cost_with_transaction_not_found(self, test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController): + """ + A transaction is running when a final cost message is sent, but the transaction is not found. This should + return a 'rejected' response on the DataTransfer message. + """ + logging.info("######### test_cost_and_price_final_cost_with_transaction_not_found #########") + + # Start transaction + await self.start_transaction(test_controller, test_utility, charge_point_v16, test_config) + + data = self.final_cost_data.copy() + # Set a non existing transaction id + data["transactionId"] = 98765 + + await charge_point_v16.data_transfer_req(vendor_id="org.openchargealliance.costmsg", message_id="FinalCost", + data=json.dumps(data)) + + #Transaction does not exist: 'rejected' must be returned. + success = await wait_for_and_validate(test_utility, charge_point_v16, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), timeout=5) + assert success + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_cost_and_price_final_cost_with_transaction(self, test_config: OcppTestConfiguration, + test_utility: TestUtility, + test_controller: TestController, probe_module, + central_system: CentralSystem): + """ + A transaction is running whan a final cost message for that transaction is sent. A session cost message + should be sent now. + """ + logging.info("######### test_cost_and_price_final_cost_with_transaction #########") + + session_cost_mock = Mock() + + probe_module.subscribe_variable("session_cost", "session_cost", session_cost_mock) + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + # Send final cost message. + await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", message_id="FinalCost", + data=json.dumps(self.final_cost_data)) + + # Which is accepted + success = await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted), timeout=5) + + received_data = {'cost_chunks': [{'cost': {'value': 33100}}], 'currency': {'decimals': 4}, 'message': [{ + 'content': 'GBP 2.81 @ 0.12/kWh, GBP 0.50 @ 1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: GBP 3.31. ' + 'Visit www.cpo.com/invoices/13546 for an invoice of your session.'}, + { + 'content': '€2.81 @ €0.12/kWh, €0.50 @ €1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: €3.31. ' + 'Bezoek www.cpo.com/invoices/13546 voor een factuur van uw laadsessie.', + 'format': 'UTF8', + 'language': 'nl'}, + { + 'content': '€2,81 @ €0,12/kWh, €0,50 @ €1/h, GESAMT-KWH: 23,4 ZEIT: 03:50 KOSTEN: €3,31. ' + 'Besuchen Sie www.cpo.com/invoices/13546 um eine Rechnung für Ihren Ladevorgang zu erhalten.', + 'format': 'UTF8', + 'language': 'de'}], + 'qr_code': 'https://www.cpo.com/invoices/13546', 'session_id': ANY, 'status': 'Finished'} + + # A session cost message should have been received + await self.await_mock_called(session_cost_mock) + + assert session_cost_mock.call_count == 1 + + # And it should contain the correct data + session_cost_mock.assert_called_once_with(received_data) + + assert success + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment()) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, probe_module, + central_system: CentralSystem): + """ + A transaction is started and a 'running cost' message with the transaction id is sent. This should send a + session cost message over the interface. + """ + logging.info("######### test_cost_and_price_running_cost #########") + + session_cost_mock = Mock() + probe_module.subscribe_variable("session_cost", "session_cost", session_cost_mock) + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + test_utility.messages.clear() + + # Send running cost message. + assert await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(self.running_cost_data)) + + # Since there is a transaction running and the correct transaction id is sent in the running cost request, + # the datatransfer message is accepted. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted)) + + # A session cost call should have been sent now with the correct data. + received_data = { + 'charging_price': [{'category': 'Time', 'price': {'currency': {'decimals': 4}, 'value': {'value': 20000}}}, + {'category': 'Energy', 'price': {'currency': {'decimals': 4}, 'value': {'value': 1230}}}, + {'category': 'FlatFee', + 'price': {'currency': {'decimals': 4}, 'value': {'value': 424200}}}], + 'cost_chunks': [ + {'cost': {'value': 13450}, 'metervalue_to': 1234000, 'timestamp_to': ANY}], + 'currency': {'decimals': 4}, + 'idle_price': {'grace_minutes': 30, 'hour_price': {'currency': {'decimals': 4}, 'value': {'value': 10000}}}, + 'next_period': { + 'charging_price': [{'category': 'Time', + 'price': {'currency': {'decimals': 4}, 'value': {'value': 40000}}}, + {'category': 'Energy', + 'price': {'currency': {'decimals': 4}, 'value': {'value': 1000}}}, + {'category': 'FlatFee', + 'price': {'currency': {'decimals': 4}, 'value': {'value': 848400}}}], + 'idle_price': {'hour_price': {'currency': {'decimals': 4}, 'value': {'value': 5000}}}, + 'timestamp_from': ANY}, + 'session_id': ANY, 'status': 'Running'} + + await self.await_mock_called(session_cost_mock) + + assert session_cost_mock.call_count == 1 + + session_cost_mock.assert_called_once_with(received_data) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_wrong_transaction(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, + charge_point_v16: ChargePoint16): + """ + A transaction is started and a running cost message is sent, but the transaction id is not known so the message + is rejected. + """ + logging.info("######### test_cost_and_price_running_cost_wrong_transaction #########") + + # Start transaction + await self.start_transaction(test_controller, test_utility, charge_point_v16, test_config) + + data = self.running_cost_data.copy() + # Set non existing transaction id. + data["transactionId"] = 42 + + # Send running cost message with incorrect transaction id. + assert await charge_point_v16.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(data)) + + # DataTransfer should return 'rejected' because the transaction is not found. + assert await wait_for_and_validate(test_utility, charge_point_v16, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), timeout=15) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_no_transaction(self, test_config: OcppTestConfiguration, + test_utility: TestUtility, + charge_point_v16: ChargePoint16): + """ + There is no transaction but there is a running cost message sent. This should return a 'rejected' on the + DataTransfer request. + """ + logging.info("######### test_cost_and_price_running_cost_no_transaction #########") + + test_utility.messages.clear() + + data = { + "transactionId": 1, + "timestamp": datetime.now(timezone.utc).isoformat(), "meterValue": 1234000, + "cost": 1.345, + "state": "Charging", + "chargingPrice": { + "kWhPrice": 0.123, "hourPrice": 0.00, "flatFee": 0.00}, + "idlePrice": {"graceMinutes": 30, "hourPrice": 1.00}, + "nextPeriod": { + "atTime": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(), + "chargingPrice": { + "kWhPrice": 0.100, "hourPrice": 0.00, "flatFee": 0.00}, + "idlePrice": {"hourPrice": 0.00} + }, + "triggerMeterValue": { + "atTime": (datetime.now(timezone.utc) + timedelta(seconds=3)).isoformat(), + "atEnergykWh": 5.0, + "atPowerkW": 8.0, + "atCPStatus": [ChargePointStatus.finishing, ChargePointStatus.available] + } + } + + # Send RunningCost message while there is no transaction. + assert await charge_point_v16.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(data)) + + # This should return 'Rejected' + assert await wait_for_and_validate(test_utility, charge_point_v16, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.rejected), timeout=15) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["evse_manager"])) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_trigger_time(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, probe_module, + central_system: CentralSystem): + """ + Send running cost with a trigger time to return meter values. + """ + logging.info("######### test_cost_and_price_running_cost_trigger_time #########") + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + }, + "power_W": { + "total": 1000.0 + } + } + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Metervalues should be sent at below trigger time. + data = self.running_cost_data.copy() + data["triggerMeterValue"]["atTime"] = (datetime.now(timezone.utc) + timedelta(seconds=3)).isoformat() + + # While the transaction is started, send a 'RunningCost' message. + assert await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(data)) + + # Which is accepted. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted)) + + # At the given time, metervalues must have been sent. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '1.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1), timeout=15) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["evse_manager"] + )) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_trigger_energy(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, probe_module, + central_system: CentralSystem): + """ + Send running cost with a trigger kwh value to return meter values. + """ + logging.info("######### test_cost_and_price_running_cost_trigger_energy #########") + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + } + } + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Send running cost, which has a trigger specified on atEnergykWh = 5.0 + assert await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(self.running_cost_data)) + + # Datatransfer is valid and should be accepted. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted)) + + # Now increase power meter value so it is above the specified trigger and publish the powermeter value + power_meter_value["energy_Wh_import"]["total"] = 6000.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # Metervalues should now be sent + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '6000.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1)) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["evse_manager"] + )) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_trigger_power(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, probe_module, + central_system: CentralSystem): + """ + Send running cost with a trigger kw value to return meter values. + """ + logging.info("######### test_cost_and_price_running_cost_trigger_power #########") + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + }, + "power_W": { + "total": 1000.0 + } + } + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Send running cost data with a trigger specified of 8 kW + assert await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(self.running_cost_data)) + + # DataTransfer message is valid, expect it's accepted. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted)) + + # Set W above the trigger value and publish a new powermeter value. + power_meter_value["energy_Wh_import"]["total"] = 1.0 + power_meter_value["power_W"]["total"] = 10000.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # Powermeter value should be sent because of the trigger. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '1.00'}, + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', 'unit': 'W', + 'value': '10000.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1)) + + # W value is below trigger, but hysteresis prevents sending the metervalue. + power_meter_value["energy_Wh_import"]["total"] = 8000.0 + power_meter_value["power_W"]["total"] = 7990.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # So no metervalue is sent. + assert not await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '8000.00'}, + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', 'unit': 'W', + 'value': '7990.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1)) + + # Only when trigger is high ( / low) enough, metervalue will be sent. + power_meter_value["energy_Wh_import"]["total"] = 9500.0 + power_meter_value["power_W"]["total"] = 7200.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '9500.00'}, + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', 'unit': 'W', + 'value': '7200.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1)) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["evse_manager"] + )) + @pytest.mark.asyncio + async def test_cost_and_price_running_cost_trigger_cp_status(self, test_config: OcppTestConfiguration, + test_controller: TestController, + test_utility: TestUtility, probe_module, + central_system: CentralSystem): + """ + Send running cost with a trigger chargepoint status to return meter values. + """ + logging.info("######### test_cost_and_price_running_cost_trigger_cp_status #########") + + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + }, + "power_W": { + "total": 1000.0 + } + } + + 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() + + # Start transaction + await self.start_transaction(test_controller, test_utility, chargepoint_with_pm, test_config) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Send data with cp status finishing and suspended ev as triggers. + data = { + "transactionId": 1, + "timestamp": datetime.now(timezone.utc).isoformat(), "meterValue": 1234000, + "cost": 1.345, + "state": "Charging", + "chargingPrice": { + "kWhPrice": 0.123, "hourPrice": 0.00, "flatFee": 0.00}, + "idlePrice": {"graceMinutes": 30, "hourPrice": 1.00}, + "nextPeriod": { + "atTime": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(), + "chargingPrice": { + "kWhPrice": 0.100, "hourPrice": 0.00, "flatFee": 0.00}, + "idlePrice": {"hourPrice": 0.00} + }, + "triggerMeterValue": { + "atCPStatus": [ChargePointStatus.finishing, ChargePointStatus.suspended_ev] + } + } + + assert await chargepoint_with_pm.data_transfer_req(vendor_id="org.openchargealliance.costmsg", + message_id="RunningCost", + data=json.dumps(data)) + + # And wait for the datatransfer to be accepted. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "DataTransfer", + call_result.DataTransferPayload(DataTransferStatus.accepted), timeout=15) + + # 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, chargepoint_with_pm, "StatusNotification", + call.StatusNotificationPayload(1, ChargePointErrorCode.no_error, + ChargePointStatus.finishing)) + + # As the chargepoint status is now 'finishing' new metervalues should be sent. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + call.MeterValuesPayload(1, meter_value=[{'sampledValue': [ + {'context': 'Other', 'format': 'Raw', 'location': 'Outlet', + 'measurand': 'Energy.Active.Import.Register', 'unit': 'Wh', + 'value': '1.00'}], 'timestamp': timestamp[:-9] + 'Z'}], + transaction_id=1)) + + # expect StopTransaction.req + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "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, chargepoint_with_pm, "StatusNotification", + call.StatusNotificationPayload(1, ChargePointErrorCode.no_error, + ChargePointStatus.available)) + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_set_price_text(self, test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility): + """ + Test 'DefaultPriceText' configuration setting. + """ + logging.info("######### test_cost_and_price_set_price_text #########") + + test_utility.validation_mode = ValidationMode.STRICT + + # First get price text for specific language. + response = await charge_point_v16.get_configuration_req(key=['DefaultPriceText,de']) + assert response.configuration_key[0]['key'] == 'DefaultPriceText,de' + assert response.configuration_key[0]['value'] == '' + + # Set price text for specific language. + price_text = { + "priceText": "€0.15 / kWh, Leerlaufgebühr nach dem Aufladen: 1 $/hr", + "priceTextOffline": "Die Station ist offline. Laden ist für €0,15/kWh möglich" + } + + response = await charge_point_v16.change_configuration_req(key="DefaultPriceText,de", value=json.dumps(price_text)) + assert response.status == "Accepted" + + # Get price text for specific language to check if it is set. + response = await charge_point_v16.get_configuration_req(key=['DefaultPriceText,de']) + + assert response.configuration_key[0]['key'] == 'DefaultPriceText,de' + assert json.loads(response.configuration_key[0]['value']) == price_text + + # Set price text for not supported language. + price_text = { + "priceText": "0,15 € / kWh, frais d'inactivité après recharge : 1 $/h" + } + response = await charge_point_v16.change_configuration_req(key="DefaultPriceText,fr", value=json.dumps(price_text)) + assert response.status == "Rejected" + + # Get price text for specific language to check if it is set. + response = await charge_point_v16.get_configuration_req(key=['DefaultPriceText,fr']) + + assert response.configuration_key[0]['key'] == 'DefaultPriceText,fr' + assert response.configuration_key[0]['value'] == '' + + @pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp16-costandprice.yaml')) + @pytest.mark.asyncio + async def test_cost_and_price_set_charging_price(self, test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility): + """ + Test 'DefaultPrice' configuration setting. + """ + logging.info("######### test_cost_and_price_set_charging_price #########") + + test_utility.validation_mode = ValidationMode.STRICT + + # First get price text for specific language. + response = await charge_point_v16.get_configuration_req(key=['DefaultPrice']) + assert response.configuration_key[0]['key'] == 'DefaultPrice' + assert response.configuration_key[0]['value'] + + # Set price text for specific language. + default_price = { + "priceText": "0.15 $/kWh, idle fee after charging: 1 $/hr", + "priceTextOffline": "The station is offline. Charging is possible for 0.15 $/kWh.", + "chargingPrice": {"kWhPrice": 0.15, "hourPrice": 0.00, "flatFee": 0.00} + } + + await charge_point_v16.change_configuration_req(key="DefaultPrice", value=json.dumps(default_price)) + + # Get price text for specific language to check if it is set. + response = await charge_point_v16.get_configuration_req(key=['DefaultPrice']) + + assert response.configuration_key[0]['key'] == 'DefaultPrice' + assert json.loads(response.configuration_key[0]['value']) == default_price 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..a1d29f904 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/ocpp_compliance_tests.py @@ -0,0 +1,6740 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Pionix GmbH and Contributors to EVerest + +from datetime import datetime, timedelta +import logging +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.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Reservation", "ReserveConnectorZeroSupported", False)]) +) +@pytest.mark.asyncio +async def test_reservation_connector_zero_not_supported( + charge_point_v16: ChargePoint16, test_utility: TestUtility, test_config: OcppTestConfiguration +): + logging.info("######### test_reservation_connector_zero_not_supported #########") + + await charge_point_v16.reserve_now_req( + connector_id=0, + 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.rejected), + ) + + +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Reservation", "ReserveConnectorZeroSupported", True)]) +) +@pytest.mark.asyncio +async def test_reservation_connector_zero_supported( + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_config: OcppTestConfiguration, + test_controller: TestController, +): + logging.info("######### test_reservation_connector_zero_supported #########") + + await charge_point_v16.reserve_now_req( + connector_id=0, + 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.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.no_error, ChargePointStatus.reserved + ), + ) + + # 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_faulted_state( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_reservation_faulted_state #########") + + test_controller.raise_error("MREC6UnderVoltage", 1) + + await asyncio.sleep(1) + + # expect StatusNotification with status faulted + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "StatusNotification", + call.StatusNotificationPayload( + 1, ChargePointErrorCode.other_error, 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), + ) + + test_controller.clear_error("MREC6UnderVoltage", 1) + + +@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_parentid( + test_config: OcppTestConfiguration, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, + test_controller: TestController, +): + logging.info("######### test_reservation_with_parentid #########") + + # 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.asyncio +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment([("Security", "SecurityProfile", 0)]) +) +async def test_chargepoint_update_security_profile( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info("######### test_chargepoint_update_security_profile #########") + + await charge_point_v16.change_configuration_req(key="SecurityProfile", value="1") + # 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(key=["SecurityProfile"]) + + assert response.configuration_key[0]["key"] == "SecurityProfile" + assert response.configuration_key[0]["value"] == "1" + + +@pytest.mark.asyncio +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment( + [ + ("Internal", "RetryBackoffRandomRange", 1), + ("Internal", "RetryBackoffWaitMinimum", 2), + ] + ) +) +async def test_chargepoint_update_security_profile_fallback( + central_system_v16: CentralSystem, + charge_point_v16: ChargePoint16, + test_utility: TestUtility, +): + logging.info( + "######### test_chargepoint_update_security_profile_fallback #########" + ) + + await charge_point_v16.change_configuration_req(key="SecurityProfile", 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), + ) + + # 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(key=["SecurityProfile"]) + + assert response.configuration_key[0]["key"] == "SecurityProfile" + assert response.configuration_key[0]["value"] == "0" + + +@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..f78ab7757 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/ocpp_generic_interface_integration_tests.py @@ -0,0 +1,740 @@ +# 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( + "security", + "get_verify_location", + "", + 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) + await asyncio.sleep(5) + 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..f4c0967d0 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp16/plug_and_charge_tests.py @@ -0,0 +1,721 @@ +# 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, ConfigurationStatus +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.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP16ConfigAdjustment +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" + + +@pytest.mark.asyncio +@pytest.mark.ocpp_config_adaptions( + GenericOCPP16ConfigAdjustment( + [("Internal", "ConnectorEvseIds", "test_value")] + ) + ) +async def test_set_connector_evse_ids( + charge_point_v16: ChargePoint16, test_utility: TestUtility +): + + initial_value = "test_value" + invalid_value = "WRONG,DE*PNX*100001" + new_valid_value = "DE*PNX*100001,DE*PNX*100002" + + await charge_point_v16.change_configuration_req( + key="ConnectorEvseIds", value=invalid_value + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.rejected), + ) + + await charge_point_v16.get_configuration_req(key=["ConnectorEvseIds"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "ConnectorEvseIds", "readonly": False, "value": initial_value}] + ), + ) + + await charge_point_v16.change_configuration_req( + key="ConnectorEvseIds", value=new_valid_value + ) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "ChangeConfiguration", + call_result.ChangeConfigurationPayload(ConfigurationStatus.accepted), + ) + + await charge_point_v16.get_configuration_req(key=["ConnectorEvseIds"]) + assert await wait_for_and_validate( + test_utility, + charge_point_v16, + "GetConfiguration", + call_result.GetConfigurationPayload( + [{"key": "ConnectorEvseIds", "readonly": False, "value": new_valid_value}] + ), + ) + 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/california_pricing.py b/tests/ocpp_tests/test_sets/ocpp201/california_pricing.py new file mode 100644 index 000000000..7decd3db9 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/california_pricing.py @@ -0,0 +1,563 @@ +from datetime import timezone +from unittest.mock import Mock, ANY + +import logging +from copy import deepcopy + +from 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 +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +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 (IdTokenType as IdTokenTypeEnum, ConnectorStatusType, + ClearCacheStatusType) +from ocpp.v201.datatypes import * +from everest.testing.ocpp_utils.fixtures import * +from everest_test_utils_probe_modules import (probe_module, + ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment, + ProbeModuleCostAndPriceSessionCostConfigurationAdjustment) + +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP201ConfigAdjustment, + OCPP201ConfigVariableIdentifier, +) + +from validations import validate_status_notification_201 + +log = logging.getLogger("ocpp201CaliforniaPricingTest") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.inject_csms_mock +@pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp201-costandprice.yaml')) +@pytest.mark.ocpp_config_adaptions(GenericOCPP201ConfigAdjustment([ + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageCtrlrAvailable", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "QRCodeDisplayCapable", + "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageLanguage", "Actual"), + "en"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableTariff", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableCost", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrEnabledTariff", "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrEnabledCost", "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "NumberOfDecimalsForCostValues", "Actual"), + "5"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageTimeout", "Actual"), + "1"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttemptInterval", + "Actual"), "1"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttempts", "Actual"), + "3"), + (OCPP201ConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheCtrlrEnabled", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("AuthCtrlr", "LocalPreAuthorize", + "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheLifeTime", "Actual"), + "86400"), + (OCPP201ConfigVariableIdentifier("CustomizationCtrlr", "CustomImplementationCaliforniaPricingEnabled", + "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("CustomizationCtrlr", "CustomImplementationMultiLanguageEnabled", + "Actual"), "true") +])) +class TestOcpp201CostAndPrice: + """ + Tests for OCPP 2.0.1 California Pricing Requirements + """ + + cost_updated_custom_data = { + "vendorId": "org.openchargealliance.costmsg", + "timestamp": datetime.now(timezone.utc).isoformat(), "meterValue": 1234000, + "state": "Charging", + "chargingPrice": {"kWhPrice": 0.123, "hourPrice": 2.00, "flatFee": 42.42}, + "idlePrice": {"graceMinutes": 30, "hourPrice": 1.00}, + "nextPeriod": { + "atTime": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(), + "chargingPrice": {"kWhPrice": 0.100, "hourPrice": 4.00, "flatFee": 84.84}, + "idlePrice": {"hourPrice": 0.50} + }, + "triggerMeterValue": { + "atTime": datetime.now(timezone.utc).isoformat(), + "atEnergykWh": 5.0, + "atPowerkW": 8.0 + } + } + + @staticmethod + async def start_transaction(test_controller: TestController, test_utility: TestUtility, + charge_point: ChargePoint201): + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + # make an unknown IdToken + id_token = IdTokenType( + id_token="DEADBEEF", + type=IdTokenTypeEnum.iso14443 + ) + + assert await wait_for_and_validate(test_utility, charge_point, "StatusNotification", + call201.StatusNotificationPayload(datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id), + validate_status_notification_201) + + # Charging station is now available, start charging session. + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + assert await wait_for_and_validate(test_utility, charge_point, "Authorize", + call201.AuthorizePayload(id_token + )) + + # start charging session + test_controller.plug_in() + + # should send a Transaction event + transaction_event = await wait_for_and_validate(test_utility, charge_point, "TransactionEvent", + {"eventType": "Started"}) + transaction_id = transaction_event['transaction_info']['transaction_id'] + + assert await wait_for_and_validate(test_utility, charge_point, "TransactionEvent", + {"eventType": "Updated"}) + + return transaction_id + + @staticmethod + async def await_mock_called(mock): + while not mock.call_count: + await asyncio.sleep(0.1) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment()) + async def test_set_running_cost(self, central_system: CentralSystem, test_controller: TestController, + test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module): + """ + Test running and final cost, that is 'embedded' in the TransactionEventResponse. + """ + # prepare data for the test + transaction_event_response_started = call_result201.TransactionEventPayload() + + transaction_event_response = call_result201.TransactionEventPayload() + transaction_event_response.total_cost = 3.13 # According to the OCPP spec this should be a floating point number but the test framework does not allow that. + transaction_event_response.updated_personal_message = {"format": "UTF8", "language": "en", + "content": "$2.81 @ $0.12/kWh, $0.50 @ $1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: $3.31. Visit www.cpo.com/invoices/13546 for an invoice of your session."} + transaction_event_response.custom_data = {"vendorId": "org.openchargealliance.org.qrcode", + "qrCodeText": "https://www.cpo.com/invoices/13546"} + + transaction_event_response_ended = deepcopy(transaction_event_response) + transaction_event_response_ended.total_cost = 55.1 + + received_data = {'cost_chunks': [{'cost': {'value': 313000}, 'timestamp_to': ANY}], + 'currency': {'code': 'EUR', 'decimals': 5}, 'message': [{ + 'content': '$2.81 @ $0.12/kWh, $0.50 @ $1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: $3.31. ' + 'Visit www.cpo.com/invoices/13546 for an invoice of your session.', + 'format': 'UTF8', 'language': 'en'}, + ], + 'qr_code': 'https://www.cpo.com/invoices/13546', 'session_id': ANY, 'status': 'Running'} + + evse_id1 = 1 + connector_id = 1 + + probe_module_mock_fn = Mock() + + probe_module.subscribe_variable("session_cost", "session_cost", probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Clear cache + r: call_result201.ClearCachePayload = await chargepoint_with_pm.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + + # make an unknown IdToken + id_token = IdTokenType( + id_token="DEADBEEF", + type=IdTokenTypeEnum.iso14443 + ) + + # Three TransactionEvents will be sent: started, updated and ended. The last two have the pricing information. + central_system.mock.on_transaction_event.side_effect = [transaction_event_response_started, # Started + transaction_event_response, # Updated + transaction_event_response_ended] # Ended + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification", + call201.StatusNotificationPayload(datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id), + validate_status_notification_201) + + # Charging station is now available, start charging session. + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize", + call201.AuthorizePayload(id_token + )) + + # start charging session + test_controller.plug_in() + + # should send a Transaction event + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent", + {"eventType": "Started"}) + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent", + {"eventType": "Updated"}) + + # A session cost message should have been received + await self.await_mock_called(probe_module_mock_fn) + probe_module_mock_fn.assert_called_once_with(received_data) + + # Now stop the transaction, this should also send a TransactionEvent (Ended) + test_controller.plug_out() + + # 'Final' costs are a bit different than the 'Running' costs. + received_data['cost_chunks'][0] = {'cost': {'value': 5510000}, 'metervalue_to': 0, 'timestamp_to': ANY} + received_data['status'] = 'Finished' + probe_module_mock_fn.call_count = 0 + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent", + {"eventType": "Ended"}) + + await self.await_mock_called(probe_module_mock_fn) + probe_module_mock_fn.assert_called_once_with(received_data) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment()) + async def test_cost_updated_request(self, central_system: CentralSystem, + test_controller: TestController, test_utility: TestUtility, + test_config: OcppTestConfiguration, probe_module): + """ + Test the 'cost updated request' with california pricing information. + """ + received_data = { + 'charging_price': [ + {'category': 'Time', 'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 200000}}}, + {'category': 'Energy', + 'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 12300}}}, + {'category': 'FlatFee', + 'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 4242000}}}], + 'cost_chunks': [ + {'cost': {'value': 134500}, 'metervalue_to': 1234000, 'timestamp_to': ANY}], + 'currency': {'code': 'EUR', 'decimals': 5}, + 'idle_price': {'grace_minutes': 30, + 'hour_price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 100000}}}, + 'next_period': { + 'charging_price': [{'category': 'Time', + 'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 400000}}}, + {'category': 'Energy', + 'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 10000}}}, + {'category': 'FlatFee', + 'price': {'currency': {'code': 'EUR', 'decimals': 5}, + 'value': {'value': 8484000}}}], + 'idle_price': {'hour_price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 50000}}}, + 'timestamp_from': ANY}, + 'session_id': ANY, 'status': 'Running'} + + session_cost_mock = Mock() + probe_module.subscribe_variable("session_cost", "session_cost", session_cost_mock) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + # make an unknown IdToken + id_token = IdTokenType( + id_token="DEADBEEF", + type=IdTokenTypeEnum.iso14443 + ) + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification", + call201.StatusNotificationPayload(datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id), + validate_status_notification_201) + + # Send cost updated request while there is no transaction: This should just forward the request There is nothing + # in the spec that sais what to do here and you can't send a 'rejected'. + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id="1", + custom_data=self.cost_updated_custom_data) + + # A session cost message should have been received + await self.await_mock_called(session_cost_mock) + session_cost_mock.assert_called_once_with(received_data) + + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize", + call201.AuthorizePayload(id_token + )) + + # start charging session + test_controller.plug_in() + + # should send a Transaction event + transaction_event = await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent", + {"eventType": "Started"}) + transaction_id = transaction_event['transaction_info']['transaction_id'] + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent", + {"eventType": "Updated"}) + + # Clear cache + r: call_result201.ClearCachePayload = await chargepoint_with_pm.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + session_cost_mock.call_count = 0 + + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id, + custom_data=self.cost_updated_custom_data) + + # A session cost message should have been received + await self.await_mock_called(session_cost_mock) + session_cost_mock.assert_called_once_with(received_data) + + # Clear cache + r: call_result201.ClearCachePayload = await chargepoint_with_pm.clear_cache_req() + assert r.status == ClearCacheStatusType.accepted + session_cost_mock.call_count = 0 + + # Set transaction id to a not existing transaction id. + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id="12345", + custom_data=self.cost_updated_custom_data) + + # A session cost message should still have been received + await self.await_mock_called(session_cost_mock) + session_cost_mock.assert_called_once_with(received_data) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["connector_1", "connector_2"])) + async def test_running_cost_trigger_time(self, central_system: CentralSystem, + test_controller: TestController, test_utility: TestUtility, + test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + }, + "power_W": { + "total": 1000.0 + } + } + + 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() + + # Start transaction + transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Metervalues should be sent at below trigger time. + data = self.cost_updated_custom_data.copy() + data["triggerMeterValue"]["atTime"] = (datetime.now(timezone.utc) + timedelta(seconds=3)).isoformat() + + # Once the transaction is started, send a 'RunningCost' message. + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id, + custom_data=data) + + # At the given time, metervalues must have been sent. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + {"evseId": 1, "meterValue": [{"sampledValue": [ + {"context": "Other", "location": "Outlet", + "measurand": "Energy.Active.Import.Register", + "unitOfMeasure": {"unit": "Wh"}, "value": 1.0}], + 'timestamp': timestamp[:-9] + 'Z'}]} + ) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["connector_1", "connector_2"] + )) + async def test_running_cost_trigger_energy(self, central_system: CentralSystem, + test_controller: TestController, test_utility: TestUtility, + test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + } + } + + 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() + + # Start transaction + transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Metervalues should be sent at below trigger time. + data = self.cost_updated_custom_data.copy() + + # Send running cost, which has a trigger specified on atEnergykWh = 5.0 + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id, + custom_data=data) + + # Now increase power meter value so it is above the specified trigger and publish the powermeter value + power_meter_value["energy_Wh_import"]["total"] = 6000.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # Powermeter value should be sent because of the trigger. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + {"evseId": 1, "meterValue": [{"sampledValue": [ + {"context": "Other", "location": "Outlet", + "measurand": "Energy.Active.Import.Register", + "unitOfMeasure": {"unit": "Wh"}, "value": 6000.0}], + 'timestamp': timestamp[:-9] + 'Z'}]} + ) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment( + evse_manager_ids=["connector_1", "connector_2"] + )) + async def test_running_cost_trigger_power(self, central_system: CentralSystem, + test_controller: TestController, test_utility: TestUtility, + test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "OK" + } + + probe_module.implement_command("ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn) + probe_module.implement_command("ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn) + + power_meter_value = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "energy_Wh_import": { + "total": 1.0 + }, + "power_W": { + "total": 1000.0 + } + } + + 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() + + # Start transaction + transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm) + + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + test_utility.messages.clear() + + # Metervalues should be sent at below trigger time. + data = self.cost_updated_custom_data.copy() + + # Send running cost, which has a trigger specified on atEnergykWh = 5.0 + await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id, + custom_data=data) + + # Set W above the trigger value and publish a new powermeter value. + power_meter_value["energy_Wh_import"]["total"] = 1.0 + power_meter_value["power_W"]["total"] = 10000.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # Powermeter value should be sent because of the trigger. + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + {"evseId": 1, "meterValue": [{"sampledValue": [ + {"context": "Other", "location": "Outlet", + "measurand": "Energy.Active.Import.Register", + "unitOfMeasure": {"unit": "Wh"}, "value": 1.0}, + {'context': 'Other', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"}, + 'value': 10000.00} + ], + 'timestamp': timestamp[:-9] + 'Z'}]} + ) + + # W value is below trigger, but hysteresis prevents sending the metervalue. + power_meter_value["power_W"]["total"] = 7990.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + # So no metervalue is sent + assert not await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + {"evseId": 1, "meterValue": [{"sampledValue": [ + {"context": "Other", "location": "Outlet", + "measurand": "Energy.Active.Import.Register", + "unitOfMeasure": {"unit": "Wh"}, "value": 1.0}, + {'context': 'Other', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"}, + 'value': 7990.0} + ], + 'timestamp': timestamp[:-9] + 'Z'}]} + ) + + # Only when trigger is high ( / low) enough, metervalue will be sent. + power_meter_value["power_W"]["total"] = 7200.0 + timestamp = datetime.now(timezone.utc).isoformat() + power_meter_value["timestamp"] = timestamp + probe_module.publish_variable("ProbeModulePowerMeter", "powermeter", power_meter_value) + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues", + {"evseId": 1, "meterValue": [{"sampledValue": [ + {"context": "Other", "location": "Outlet", + "measurand": "Energy.Active.Import.Register", + "unitOfMeasure": {"unit": "Wh"}, "value": 1.0}, + {'context': 'Other', 'location': 'Outlet', + 'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"}, + 'value': 7200.00} + ], + 'timestamp': timestamp[:-9] + 'Z'}]} + ) 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/display_message.py b/tests/ocpp_tests/test_sets/ocpp201/display_message.py new file mode 100644 index 000000000..42a9331e9 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/display_message.py @@ -0,0 +1,391 @@ +from datetime import timezone +from unittest.mock import Mock + +import pytest + +import logging + +from everest.testing.ocpp_utils.central_system import CentralSystem + +from 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.controller.test_controller_interface import TestController + +from everest_test_utils_probe_modules import (probe_module, + ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment) + +from ocpp.v201 import call as call201 +from ocpp.v201 import call_result as call_result201 +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum, ConnectorStatusType) +from ocpp.v201.datatypes import * + +from everest.testing.core_utils._configuration.libocpp_configuration_helper import ( + GenericOCPP201ConfigAdjustment, + OCPP201ConfigVariableIdentifier, +) +from validations import validate_status_notification_201 + +log = logging.getLogger("ocpp201DisplayMessageTest") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.inject_csms_mock +@pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp201-costandprice.yaml')) +@pytest.mark.ocpp_config_adaptions(GenericOCPP201ConfigAdjustment([ + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageCtrlrAvailable", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "QRCodeDisplayCapable", + "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageLanguage", "Actual"), + "en"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableTariff", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableCost", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrEnabledTariff", "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrEnabledCost", "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("TariffCostCtrlr", "NumberOfDecimalsForCostValues", "Actual"), + "5"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageTimeout", "Actual"), + "1"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttemptInterval", + "Actual"), "1"), + (OCPP201ConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttempts", "Actual"), + "3"), + (OCPP201ConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheCtrlrEnabled", "Actual"), + "true"), + (OCPP201ConfigVariableIdentifier("AuthCtrlr", "LocalPreAuthorize", + "Actual"), "true"), + (OCPP201ConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheLifeTime", "Actual"), + "86400"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedPriorities", + "Actual"), "AlwaysFront,NormalCycle"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedFormats", + "Actual"), "ASCII,URI,UTF8"), + (OCPP201ConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedStates", "Actual"), + "Charging,Faulted,Unavailable") +])) +class TestOcpp201CostAndPrice: + """ + Tests for OCPP 2.0.1 Display Message + """ + + @staticmethod + async def start_transaction(test_controller: TestController, test_utility: TestUtility, + charge_point: ChargePoint201): + # prepare data for the test + evse_id1 = 1 + connector_id = 1 + + # make an unknown IdToken + id_token = IdTokenType( + id_token="DEADBEEF", + type=IdTokenTypeEnum.iso14443 + ) + + assert await wait_for_and_validate(test_utility, charge_point, "StatusNotification", + call201.StatusNotificationPayload(datetime.now().isoformat(), + ConnectorStatusType.available, + evse_id=evse_id1, + connector_id=connector_id), + validate_status_notification_201) + + # Charging station is now available, start charging session. + # swipe id tag to authorize + test_controller.swipe(id_token.id_token) + assert await wait_for_and_validate(test_utility, charge_point, "Authorize", + call201.AuthorizePayload(id_token + )) + + # start charging session + test_controller.plug_in() + + # should send a Transaction event + transaction_event = await wait_for_and_validate(test_utility, charge_point, "TransactionEvent", + {"eventType": "Started"}) + transaction_id = transaction_event['transaction_info']['transaction_id'] + + assert await wait_for_and_validate(test_utility, charge_point, "TransactionEvent", + {"eventType": "Updated"}) + + return transaction_id + + @staticmethod + async def await_mock_called(mock): + while not mock.call_count: + await asyncio.sleep(0.1) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + async def test_set_display_message(self, central_system: CentralSystem, test_controller: TestController, + test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + start_time = datetime.now(timezone.utc).isoformat() + end_time = (datetime.now(timezone.utc) + timedelta(minutes=1)).isoformat() + + message = {'id': 1, 'priority': 'NormalCycle', + 'message': {'format': 'UTF8', 'language': 'en', + 'content': 'This is a display message'}, + 'startDateTime': start_time, + 'endDateTime': end_time} + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + + # Display message should have received a message with the current price information + data_received = { + 'request': [{'id': 1, 'identifier_type': 'TransactionId', + 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'}, + 'priority': 'NormalCycle', 'timestamp_from': start_time[:-9] + 'Z', + 'timestamp_to': end_time[:-9] + 'Z'}] + } + + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='Accepted'), + timeout=5) + probe_module_mock_fn.assert_called_once_with(data_received) + + # Test rejected return value + probe_module_mock_fn.return_value = { + "status": "Rejected" + } + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='Rejected'), + timeout=5) + + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + # Test unsupported priority + message['priority'] = 'InFront' + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='NotSupportedPriority'), + timeout=5) + message['priority'] = 'NormalCycle' + + # Test unsupported message format + message['message']['format'] = 'HTML' + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='NotSupportedMessageFormat'), + timeout=5) + message['message']['format'] = 'UTF8' + + # Test unsupported state + message['state'] = 'Idle' + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='NotSupportedState'), + timeout=5) + + message['state'] = 'Charging' + + # Test unknown transaction + message['transactionId'] = '12345' + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='UnknownTransaction'), + timeout=5) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + async def test_set_display_message_with_transaction(self, central_system: CentralSystem, + test_controller: TestController, + test_utility: TestUtility, test_config: OcppTestConfiguration, + probe_module): + probe_module_mock_fn = Mock() + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm) + + message = {'transactionId': transaction_id, 'id': 1, 'priority': 'NormalCycle', + 'message': {'format': 'UTF8', 'language': 'en', + 'content': 'This is a display message'}} + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + + # Display message should have received a message with the current price information + data_received = { + 'request': [{'id': 1, 'identifier_id': transaction_id, 'identifier_type': 'TransactionId', + 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'}, + 'priority': 'NormalCycle'}] + } + + probe_module_mock_fn.assert_called_once_with(data_received) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='Accepted'), + timeout=5) + + # Test unknown transaction + message['transactionId'] = '12345' + + await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage", + call_result201.SetDisplayMessagePayload(status='UnknownTransaction'), + timeout=5) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + async def test_get_display_messages(self, central_system: CentralSystem, test_controller: TestController, + test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # No messages should return 'unknown' + probe_module_mock_fn.return_value = { + "messages": [] + } + + await chargepoint_with_pm.get_display_nessages_req(request_id=1) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage", + call_result201.GetDisplayMessagesPayload(status='Unknown'), + timeout=5) + + # At least one message should return 'accepted' + probe_module_mock_fn.return_value = { + "messages": [ + {'id': 1, 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'}, + 'priority': 'InFront'} + ] + } + + await chargepoint_with_pm.get_display_nessages_req(id=[1], request_id=1) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage", + call_result201.GetDisplayMessagesPayload(status='Accepted'), + timeout=5) + + assert await \ + wait_for_and_validate(test_utility, chargepoint_with_pm, "NotifyDisplayMessages", + call201.NotifyDisplayMessagesPayload(request_id=1, + message_info=[{"id": 1, + "message": { + "content": "This is a " + "display message", + "format": "UTF8", + "language": "en"}, + "priority": "InFront"}])) + + # Return multiple messages + probe_module_mock_fn.return_value = { + "messages": [ + {'id': 1, 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'}, + 'priority': 'InFront'}, + {'id': 2, 'message': {'content': 'This is a display message 2', 'format': 'UTF8', 'language': 'en'}, + 'priority': 'NormalCycle'} + ] + } + + await chargepoint_with_pm.get_display_nessages_req(request_id=1) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage", + call_result201.GetDisplayMessagesPayload(status='Accepted'), + timeout=5) + + assert await \ + wait_for_and_validate(test_utility, chargepoint_with_pm, "NotifyDisplayMessages", + call201.NotifyDisplayMessagesPayload(request_id=1, + message_info=[{"id": 1, + "message": { + "content": "This is a " + "display message", + "format": "UTF8", + "language": "en"}, + "priority": "InFront"}, {"id": 2, + "message": { + "content": "This is a " + "display message 2", + "format": "UTF8", + "language": "en"}, + "priority": "NormalCycle"} + ])) + + @pytest.mark.asyncio + @pytest.mark.probe_module + @pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment()) + async def test_clear_display_messages(self, central_system: CentralSystem, test_controller: TestController, + test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module): + probe_module_mock_fn = Mock() + + probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages", + probe_module_mock_fn) + probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message", + probe_module_mock_fn) + + probe_module.start() + await probe_module.wait_to_be_ready() + + chargepoint_with_pm = await central_system.wait_for_chargepoint() + + # Clear display message is accepted + probe_module_mock_fn.return_value = { + "status": "Accepted" + } + + await chargepoint_with_pm.clear_display_message_req(id=1) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "ClearDisplayMessage", + call_result201.ClearDisplayMessagePayload(status='Accepted'), + timeout=5) + + # Clear display message returns unknown + probe_module_mock_fn.return_value = { + "status": "Unknown" + } + + await chargepoint_with_pm.clear_display_message_req(id=1) + assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "ClearDisplayMessage", + call_result201.ClearDisplayMessagePayload(status='Unknown'), + timeout=5) 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/reservations.py b/tests/ocpp_tests/test_sets/ocpp201/reservations.py new file mode 100644 index 000000000..74e6a94c6 --- /dev/null +++ b/tests/ocpp_tests/test_sets/ocpp201/reservations.py @@ -0,0 +1,1395 @@ +import pytest +import logging +from unittest.mock import ANY + +from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201 +from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility +from everest.testing.ocpp_utils.fixtures import * +from everest.testing.core_utils.controller.test_controller_interface import TestController +from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP201ConfigAdjustment +from everest_test_utils import * + +from ocpp.v201.enums import (IdTokenType as IdTokenTypeEnum, ReserveNowStatusType, ConnectorStatusType, + OperationalStatusType, CancelReservationStatusType, SetVariableStatusType, + RequestStartStopStatusType) +from ocpp.v201.datatypes import * +from ocpp.v201 import call as call_201 +from ocpp.v201 import call_result as call_result201 +from validations import (validate_remote_start_stop_transaction, wait_for_callerror_and_validate) +from ocpp.routing import on, create_route_map + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_local_start_tx( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test making a reservation and start a transaction on the reserved evse id with the correct id token. + """ + logging.info("######### test_reservation_local_start_tx #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # 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 available (reservation is now used) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.available, 1, 1 + ), + ) + + # start charging session + test_controller.plug_in() + + # expect TransactionEvent with event type Started and the reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 0} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_local_start_tx_plugin_first( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test making a reservation and start a transaction on the reserved evse id with the correct id token. + """ + logging.info("######### test_reservation_local_start_tx #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + test_utility.messages.clear() + + # start charging session + test_controller.plug_in() + + await asyncio.sleep(2) + + test_utility.messages.clear() + + # swipe valid id tag that belongs to this reservation to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect StatusNotification with status available (reservation is now used) + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.occupied, 1, 1 + ), + ) + + # expect TransactionEvent with event type Started and the reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 0} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_plug_in_other_idtoken( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test making a reservation and start a transaction on the reserved evse id with the wrong id token, plug in first. + """ + logging.info("######### test_reservation_plug_in_other_idtoken #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + test_utility.messages.clear() + + # start charging session + test_controller.plug_in() + + # No StatusNotification with status occupied should be sent + assert not await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.occupied, 1, 1 + ), + timeout=5 + ) + + test_utility.messages.clear() + + # swipe invalid id tag + test_controller.swipe(test_config.authorization_info.valid_id_tag_2) + + assert not await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.occupied, 1, 1 + ), + timeout=5 + ) + + test_utility.messages.clear() + + # swipe valid id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # expect TransactionEvent with event type Started and the reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 0} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_remote_start_tx( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test making a reservation and start a remote transaction on the reserved evse id with the correct id token. + """ + logging.info("######### test_reservation_remote_start_tx #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + # Make reservation for evse id 1. + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # send start transaction request + await charge_point_v201.request_start_transaction_req( + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), remote_start_id=1, evse_id=1 + ) + + # Which should be accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload(status="Accepted"), + validate_remote_start_stop_transaction, + ) + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status available (because reservation is 'used') + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.available, 1, 1 + ), + ) + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + # expect StartTransaction with the given reservation id + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 0} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_connector_expire( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test that a reservation can expire. + """ + logging.info("######### test_reservation_connector_expire #########") + + # Make a reservation with an expiry time ten seconds from now. + t = datetime.utcnow() + timedelta(seconds=10) + + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # Reservation is accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # Request to start transaction for the reserved evse but with another id token. + await charge_point_v201.request_start_transaction_req( + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_2, + type=IdTokenTypeEnum.iso14443), remote_start_id=1, evse_id=1 + ) + + # This will not succeed. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload(status="Rejected"), + validate_remote_start_stop_transaction, + ) + + # So we wait until the reservation is expired. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReservationStatusUpdate", + call_201.ReservationStatusUpdatePayload( + 5, "Expired" + ), + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.available, 1, 1 + ), + ) + + # Try to start a transaction now on the previously reserved evse (which reservation is now expired). + await charge_point_v201.request_start_transaction_req( + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_2, + type=IdTokenTypeEnum.iso14443), remote_start_id=1, evse_id=1 + ) + + # Now it succeeds. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload(status="Accepted"), + validate_remote_start_stop_transaction, + ) + + # Start charging session + test_controller.plug_in() + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + # expect TransactionEvent with event type 'Started' + 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"} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_connector_faulted( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test if an evse can be reserved when the evse status is 'Faulted' + """ + logging.info("######### test_reservation_connector_faulted #########") + + # Set evse in state 'faulted' + test_controller.raise_error("MREC6UnderVoltage", 1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Faulted', 1, 1 + ), + ) + + await asyncio.sleep(1) + + t = datetime.utcnow() + timedelta(minutes=10) + + # Send a reserve new request + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # which should return 'faulted' + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.faulted), + ) + + test_controller.clear_error() + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Available', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_connector_faulted_after_reservation( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test if a reservation is cancelled after the evse status is 'Faulted' + """ + logging.info("######### test_reservation_connector_faulted_after_reservation #########") + + t = datetime.utcnow() + timedelta(minutes=10) + await charge_point_v201.reserve_now_req( + id=42, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # Set evse in state 'faulted' + test_controller.raise_error("MREC6UnderVoltage", 1) + + # This should result in the reservation being cancelled. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReservationStatusUpdate", + call_201.ReservationStatusUpdatePayload( + 42, "Removed" + ), + ) + + test_controller.clear_error() + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Available', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_connector_occupied( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Try to make a reservation while a evse is occupied. + """ + logging.info("######### test_reservation_connector_occupied #########") + + # start charging session + test_controller.plug_in() + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + t = datetime.utcnow() + timedelta(minutes=10) + + await asyncio.sleep(2) + + # Request reservation + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.occupied), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_connector_unavailable( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, +): + """ + Test making a reservation with an unavailable connector, this should return 'unavailable' on a reserve now request. + """ + logging.info("######### test_reservation_connector_unavailable #########") + + # Set evse id 1 to inoperative. + await charge_point_v201.change_availablility_req( + evse={'id': 1}, operational_status=OperationalStatusType.inoperative + ) + + t = datetime.utcnow() + timedelta(seconds=30) + + # Make a reservation for evse id 1. + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # Which should fail (ReserveNow response 'Unavailable'). + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.unavailable), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "ReservationCtrlr", "ReservationCtrlrAvailable", "Actual" + ), + "false", + ) + ] + ) +) +async def test_reservation_connector_rejected( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, +): + """ + Test if reservation is rejected with the reservation ctrlr is not available. + """ + logging.info("######### test_reservation_connector_rejected #########") + + t = datetime.utcnow() + timedelta(seconds=10) + + # Try to make reservation. + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + assert await wait_for_callerror_and_validate(test_utility, + charge_point_v201, + "NotImplemented") + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "ReservationCtrlr", "ReservationCtrlrNonEvseSpecific", "Actual" + ), + "false", + ) + ] + ) +) +async def test_reservation_non_evse_specific_rejected( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, +): + """ + Try to make a non-evse specific reservation, while that is not allowed according to the settings. That should fail. + """ + logging.info("######### test_reservation_non_evse_specific_rejected #########") + + t = datetime.utcnow() + timedelta(seconds=30) + + # Try to make a reservation without evse id. + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat() + ) + + # expect ReserveNow respone with status rejected + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.rejected), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "ReservationCtrlr", "ReservationCtrlrNonEvseSpecific", "Actual" + ), + "true", + ) + ] + ) +) +async def test_reservation_non_evse_specific_accepted( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, +): + """ + Try to make a non evse specific reservation. This should succeed, according to the settings (devicemodel). + """ + logging.info("######### test_reservation_non_evse_specific_accepted #########") + + t = datetime.utcnow() + timedelta(seconds=30) + + # Make reservation without evse id. + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat() + ) + + # This should be accepted. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # swipe valid id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # start charging session + test_controller.plug_in() + + # expect TransactionEvent with event type 'Started' and the correct reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 5} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + # expect StatusNotification with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +@pytest.mark.ocpp_config_adaptions( + GenericOCPP201ConfigAdjustment( + [ + ( + OCPP201ConfigVariableIdentifier( + "ReservationCtrlr", "ReservationCtrlrNonEvseSpecific", "Actual" + ), + "true", + ) + ] + ) +) +async def test_reservation_non_evse_specific_accepted_multiple( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, +): + """ + Try to make a non evse specific reservation. This should succeed, according to the settings (devicemodel). + When making multiple reservations, as soon as there are as many reservations as evse's available, the evse's should + go to occupied. + """ + logging.info("######### test_reservation_non_evse_specific_accepted_multiple #########") + + t = datetime.utcnow() + timedelta(seconds=30) + + # Make reservation + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat() + ) + + # Expect it to be accepted. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # Make another reservation with another reservation id. + await charge_point_v201.reserve_now_req( + id=6, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat() + ) + + # expect This should be accepted as well. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # There are now as many reservations as evse's, so all evse's go to 'reserved'. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 2, 1 + ), + ) + + # There are now as many reservations as evse's, so another reservation is not possible. + await charge_point_v201.reserve_now_req( + id=7, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_2, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat() + ) + + # expect ReserveNow response with status occupied + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.occupied), + ) + + # swipe valid id tag to authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # start charging session + test_controller.plug_in() + + # expect TransactionEvent 'Started' with the correct reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started", "reservationId": 5} + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + # expect StatusNotification with status charging + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + # swipe id tag to de-authorize + test_controller.swipe(test_config.authorization_info.valid_id_tag_1) + + # stop charging session + test_controller.plug_out() + + # expect TransactionEvent 'Ended' with the correct reservation id. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended"} + ) + + # expect StatusNotification with status 'available' for both evse's as there are now less reservations than + # available evse's. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Available', 1, 1 + ), + ) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Available', 2, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_faulted_state( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, +): + """ + Test if making a reservation is possible if the evse is in faulted state. + """ + logging.info("######### test_reservation_faulted_state #########") + + test_controller.raise_error("MREC6UnderVoltage", 1) + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Faulted', 1, 1 + ), + ) + + await asyncio.sleep(1) + + t = datetime.utcnow() + timedelta(seconds=30) + + # Try to make the reservation. + await charge_point_v201.reserve_now_req( + id=0, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status 'Faulted' + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.faulted), + ) + + test_controller.clear_error("MREC6UnderVoltage", 1) + + test_utility.messages.clear() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Available', 1, 1 + ), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_cancel( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_controller: TestController, + test_utility: TestUtility, +): + """ + Test if a reservation can be cancelled. + """ + logging.info("######### test_reservation_cancel #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + # Make the reservation + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # Expect it to be accepted. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification request with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # Cancel the reservation. + await charge_point_v201.cancel_reservation_req(reservation_id=5) + + # expect CancelReservation response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "CancelReservation", + call_result201.CancelReservationPayload(status=CancelReservationStatusType.accepted), + ) + + # expect StatusNotification with status available + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.available, 1, 1 + ), + ) + + # start charging session + test_controller.plug_in() + + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, 'Occupied', 1, 1 + ), + ) + + # send request start transaction + await charge_point_v201.request_start_transaction_req( + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), remote_start_id=1, evse_id=1 + ) + + # Which should be accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload(status="Accepted"), + validate_remote_start_stop_transaction, + ) + + # expect TransactionEvent with eventType 'Started' + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started"} + ) + + # expect TransactionEvent with status 'Updated' + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_cancel_rejected( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, +): + """ + Try to cancel a non existing reservation + """ + logging.info("######### test_reservation_cancel_rejected #########") + + t = datetime.utcnow() + timedelta(minutes=10) + + # Make a reservation with reservation id 5 + await charge_point_v201.reserve_now_req( + id=5, + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + expiry_date_time=t.isoformat(), + evse_id=1 + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # Try to cancel reservation with reservation id 2, which does not exist. + await charge_point_v201.cancel_reservation_req(reservation_id=2) + + # expect CancelReservation response with status rejected + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "CancelReservation", + call_result201.CancelReservationPayload(status=CancelReservationStatusType.rejected), + ) + + +@pytest.mark.asyncio +@pytest.mark.ocpp_version("ocpp2.0.1") +async def test_reservation_with_parentid( + test_config: OcppTestConfiguration, + charge_point_v201: ChargePoint201, + test_utility: TestUtility, + test_controller: TestController, + central_system_v201: CentralSystem +): + """ + Test reservation with parent id. + """ + logging.info("######### test_reservation_with_parentid #########") + + # authorize.conf with parent id tag + @on(Action.Authorize) + def on_authorize(**kwargs): + id_tag_info = IdTokenInfoType( + status=AuthorizationStatusType.accepted, + group_id_token=IdTokenType( + id_token=test_config.authorization_info.parent_id_tag, type=IdTokenTypeEnum.iso14443 + ), + ) + return call_result201.AuthorizePayload(id_token_info=id_tag_info) + + setattr(charge_point_v201, "on_authorize", on_authorize) + central_system_v201.chargepoint.route_map = create_route_map( + central_system_v201.chargepoint + ) + charge_point_v201.route_map = create_route_map(charge_point_v201) + + 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 + + # Disable remote authorization so an 'Authorize' request is sent when starting remotely. + r: call_result201.SetVariablesPayload = ( + await charge_point_v201.set_config_variables_req( + "AuthCtrlr", "DisableRemoteAuthorization", "false" + ) + ) + + set_variable_result: SetVariableResultType = SetVariableResultType( + **r.set_variable_result[0] + ) + assert set_variable_result.attribute_status == SetVariableStatusType.accepted + + t = datetime.utcnow() + timedelta(minutes=10) + + # Make a new reservation. + await charge_point_v201.reserve_now_req( + evse_id=1, + expiry_date_time=t.isoformat(), + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_1, + type=IdTokenTypeEnum.iso14443), + group_id_token=IdTokenType(id_token=test_config.authorization_info.parent_id_tag, + type=IdTokenTypeEnum.iso14443), + id=0, + ) + + # expect ReserveNow response with status accepted + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "ReserveNow", + call_result201.ReserveNowPayload(ReserveNowStatusType.accepted), + ) + + # expect StatusNotification request with status reserved + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "StatusNotification", + call_201.StatusNotificationPayload( + ANY, ConnectorStatusType.reserved, 1, 1 + ), + ) + + # start charging session + test_controller.plug_in() + + # send request start transaction for another id tag than the one from the reservation. + await charge_point_v201.request_start_transaction_req( + id_token=IdTokenType(id_token=test_config.authorization_info.valid_id_tag_2, + type=IdTokenTypeEnum.iso14443), remote_start_id=1, evse_id=1 + ) + + # This is accepted because the reservation has a group id token. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStartTransaction", + call_result201.RequestStartTransactionPayload(status="Accepted"), + validate_remote_start_stop_transaction, + ) + + # expect Authorize request. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "Authorize", + call_201.AuthorizePayload(IdTokenType(id_token=test_config.authorization_info.valid_id_tag_2, + type=IdTokenTypeEnum.iso14443)), + ) + + # Authorize was accepted because of the correct group id token, transaction is started. + r: call_201.TransactionEventPayload = call_201.TransactionEventPayload( + **await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Started"} + ) + ) + + transaction = TransactionType(**r.transaction_info) + + # expect TransactionEvent with eventType 'Updated' + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Updated"} + ) + + # send request stop transaction + await charge_point_v201.request_stop_transaction_req( + transaction_id=transaction.transaction_id + ) + + # Which should be accepted. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "RequestStopTransaction", + call_result201.RequestStartTransactionPayload( + status=RequestStartStopStatusType.accepted + ), + ) + + # And the session should end. + assert await wait_for_and_validate( + test_utility, + charge_point_v201, + "TransactionEvent", + {"eventType": "Ended"} + ) 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..b1199c2f1 --- /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/third-party/bazel/BUILD.libmodbus.bazel b/third-party/bazel/BUILD.libmodbus.bazel deleted file mode 100644 index bfc5b0be0..000000000 --- a/third-party/bazel/BUILD.libmodbus.bazel +++ /dev/null @@ -1,21 +0,0 @@ -cc_library( - name = "libmodbus_connection", - srcs = glob(["lib/connection/src/**/*.cpp"]), - hdrs = glob(["lib/connection/include/**/*.hpp"]), - strip_include_prefix = "lib/connection/include", - deps = [ - "@@com_github_everest_liblog//:liblog", - ] -) - -cc_library( - name = "libmodbus", - srcs = glob(["src/**/*.cpp"]), - hdrs = glob(["include/**/*.hpp"]), - deps = [ - "@com_github_everest_liblog//:liblog", - ":libmodbus_connection", - ], - strip_include_prefix = "include", - visibility = ["//visibility:public"], -) diff --git a/third-party/bazel/repos.bzl b/third-party/bazel/repos.bzl index 42bc4cfcd..21cd603c1 100644 --- a/third-party/bazel/repos.bzl +++ b/third-party/bazel/repos.bzl @@ -53,7 +53,6 @@ def everest_core_repos(): name = "edm_deps", dependencies_yaml = "@everest-core//:dependencies.yaml", build_files = [ - "@everest-core//third-party/bazel:BUILD.libmodbus.bazel", "@everest-core//third-party/bazel:BUILD.libtimer.bazel", "@everest-core//third-party/bazel:BUILD.pugixml.bazel", "@everest-core//third-party/bazel:BUILD.sigslot.bazel", 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/evse_security.yaml b/types/evse_security.yaml index 5fdce9f00..3e5a90e4f 100644 --- a/types/evse_security.yaml +++ b/types/evse_security.yaml @@ -227,6 +227,9 @@ types: key: description: The path of the PEM or DER encoded private key type: string + certificate_root: + description: The PEM of the root certificate that issued this leaf + type: string certificate: description: The path of the PEM or DER encoded certificate chain type: string @@ -260,4 +263,22 @@ types: description: The requested info type: object $ref: /evse_security#/CertificateInfo + GetCertificateFullInfoResult: + description: Response to the command get_all_valid_certificates_info + type: object + required: + - status + - info + properties: + status: + description: The status of the requested command + type: string + $ref: /evse_security#/GetCertificateInfoStatus + info: + description: The requested info + type: array + items: + minimum: 0 + type: object + $ref: /evse_security#/CertificateInfo diff --git a/types/iso15118_charger.yaml b/types/iso15118_charger.yaml index 9ae161a12..d8a2893b4 100644 --- a/types/iso15118_charger.yaml +++ b/types/iso15118_charger.yaml @@ -100,6 +100,26 @@ types: - CertificateInstallationRes - CertificateUpdateReq - CertificateUpdateRes + - AuthorizationSetupReq + - AuthorizationSetupRes + - ScheduleExchangeReq + - ScheduleExchangeRes + - ServiceSelectionReq + - ServiceSelectionRes + - AcChargeLoopReq + - AcChargeLoopRes + - AcChargeParameterDiscoveryReq + - AcChargeParameterDiscoveryRes + - DcCableCheckReq + - DcCableCheckRes + - DcChargeLoopReq + - DcChargeLoopRes + - DcChargeParameterDiscoveryReq + - DcChargeParameterDiscoveryRes + - DcPreChargeReq + - DcPreChargeRes + - DcWeldingDetectionReq + - DcWeldingDetectionRes - UnknownMessage SaeJ2847BidiMode: description: Bidi mode for sae j2847_2 @@ -170,13 +190,24 @@ types: type: number minimum: 0 maximum: 1500 + evse_maximum_discharge_current_limit: + description: Maximum discharge current the EVSE can deliver in A + type: number + minimum: 0 + maximum: 10000 + evse_maximum_discharge_power_limit: + description: Maximum discharge power the EVSE can deliver in W + type: number + minimum: 0 + maximum: 1000000 DcEvseMinimumLimits: - description: Minimum Values (current and voltage) the EVSE can deliver + description: Minimum Values the EVSE can deliver type: object additionalProperties: false required: - evse_minimum_current_limit - evse_minimum_voltage_limit + - evse_minimum_power_limit properties: evse_minimum_current_limit: description: Minimum current the EVSE can deliver with the expected accuracy in A @@ -188,6 +219,21 @@ types: type: number minimum: 0 maximum: 1500 + evse_minimum_power_limit: + description: Minimum power the EVSE can deliver with the expected accuracy in W + type: number + minimum: 0 + maximum: 1000000 + evse_minimum_discharge_current_limit: + description: Minimum discharge current the EVSE can deliver in A + type: number + minimum: 0 + maximum: 10000 + evse_minimum_discharge_power_limit: + description: Minimum discharge power the EVSE can deliver in W + type: number + minimum: 0 + maximum: 1000000 SetupPhysicalValues: description: >- Initial physical values for setup a AC or DC charging session @@ -375,25 +421,25 @@ types: type: object additionalProperties: false required: - - v2g_message_id + - id properties: - v2g_message_id: + id: description: This element contains the id of the v2g message body type: string $ref: /iso15118_charger#/V2gMessageId - v2g_message_xml: + xml: description: Contains the decoded EXI stream as V2G message XML file type: string minLength: 0 - v2g_message_json: + v2g_json: description: Contains the decoded EXI stream as V2G message JSON file type: string minLength: 0 - v2g_message_exi_hex: + exi: description: Contains the EXI stream as hex string type: string minLength: 0 - v2g_message_exi_base64: + exi_base64: description: Contains the EXI stream as base64 string type: string minLength: 0 @@ -481,3 +527,139 @@ types: description: This contains the responder URL type: string maxLength: 512 + SupportedEnergyMode: + description: Supported energy mode and if the mode supports bidirectional + type: object + additionalProperties: false + required: + - energy_transfer_mode + - bidirectional + properties: + energy_transfer_mode: + description: The energy mode supported by the SECC + type: string + $ref: /iso15118_charger#/EnergyTransferMode + bidirectional: + description: Set true if the powersupply (AC or DC) supports bidi mode + type: boolean + DisplayParameters: + description: Parameters that may be displayed on the EVSE + type: object + additionalProperties: false + properties: + present_soc: + description: Current SoC of the EV battery + type: number + minimum: 0 + maximum: 100 + minimum_soc: + description: Minimum SoC EV needs after charging + type: number + minimum: 0 + maximum: 100 + target_soc: + description: Target SoC EV needs after charging + type: number + minimum: 0 + maximum: 100 + maximum_soc: + description: The SoC at which the EV will prohibit + type: number + minimum: 0 + maximum: 100 + remaining_time_to_minimum_soc: + description: Remaining time it takes to reach minimum SoC + type: number + minimum: 0 + remaining_time_to_target_soc: + description: Remaining time it takes to reach target SoC + type: number + minimum: 0 + remaining_time_to_maximum_soc: + description: Remaining time it takes to reach maximum SoC + type: number + minimum: 0 + charging_complete: + description: Indication if the charging is complete + type: boolean + battery_energy_capacity: + description: Energy capacity in Wh of the EV battery + type: number + minimum: 0 + inlet_hot: + description: Inlet temperature is too high + type: boolean + DcChargeDynamicModeValues: + description: Parameters for dynamic control mode + type: object + additionalProperties: false + required: + - target_energy_request + - max_energy_request + - min_energy_request + - max_charge_power + - min_charge_power + - max_charge_current + - max_voltage + - min_voltage + properties: + departure_time: + description: The time when the EV wants to finish charging + type: number + minimum: 0 + target_energy_request: + description: Energy request to fulfil the target SoC + type: number + minimum: 0 + max_energy_request: + description: Maximum acceptable energy level of the EV + type: number + minimum: 0 + min_energy_request: + description: Energy request to fulfil the minimum SoC + type: number + minimum: 0 + max_charge_power: + description: Maximum charge power allowed by the EV + type: number + minimum: 0 + min_charge_power: + description: Minimum charge power allowed by the EV + type: number + minimum: 0 + max_charge_current: + description: Maximum charge current allowed by the EV + type: number + minimum: 0 + max_voltage: + description: Maximum voltage allowed by the EV + type: number + minimum: 0 + min_voltage: + description: Minimum voltage allowd by the EV + type: number + minimum: 0 + max_discharge_power: + description: Maximum discharge current allowed by the EV + type: number + minimum: 0 + min_discharge_power: + description: Minimum discharge current allowed by the EV + type: number + minimum: 0 + max_discharge_current: + description: Maximum discharge current allowed by the EV + type: number + minimum: 0 + max_v2x_energy_request: + description: >- + Energy which may be charged until the PresentSOC has left the range + dedicated for cycling activity. + type: number + minimum: 0 + min_v2x_energy_request: + description: >- + Energy which needs to be charged until the PresentSOC has left the + range dedicated for cycling activity. + type: number + minimum: 0 diff --git a/types/ocpp.yaml b/types/ocpp.yaml index 74db5f0b6..ca77d3fed 100644 --- a/types/ocpp.yaml +++ b/types/ocpp.yaml @@ -73,14 +73,28 @@ types: description: schedule for a connector type: object $ref: /ocpp#/ChargingSchedule + EVSE: + description: >- + Type of an EVSE. If only the id is present, this type identifies an EVSE. + If also a connector_id is given, this type identifies a Connector of the EVSE + type: object + required: + - id + properties: + id: + description: Id of the EVSE + type: integer + minimum: 1 + connector_id: + description: An id to designate a specific connector (on an EVSE) by connector index number + type: integer + minimum: 1 OcppTransactionEvent: description: >- Element providing information on OCPP transactions. type: object required: - transaction_event - - evse_id - - connector - session_id properties: transaction_event: @@ -88,14 +102,11 @@ types: The transaction related event. type: string $ref: /ocpp#/TransactionEvent - evse_id: - description: >- - The OCPP 2.0.1 EVSE ID associated with the transaction. - type: integer - connector: + evse: description: >- - The connector associated with the transaction. - type: integer + The OCPP 2.0.1 EVSE associated with the transaction. + type: object + $ref: /ocpp#/EVSE session_id: description: >- The EVSE manager assigned session ID. @@ -271,22 +282,6 @@ types: description: Timestamp of the moment the security event was generated, if absent the current datetime is assumed type: string format: date-time - EVSE: - description: >- - Type of an EVSE. If only the id is present, this type identifies an EVSE. - If also a connector_id is given, this type identifies a Connector of the EVSE - type: object - required: - - id - properties: - id: - description: Id of the EVSE - type: integer - minimum: 1 - connector_id: - description: An id to designate a specific connector (on an EVSE) by connector index number - type: integer - minimum: 1 Variable: description: >- Type for a variable with a name and an optional instance diff --git a/types/reservation.yaml b/types/reservation.yaml index b15514786..168a27997 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,17 +44,25 @@ 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 Expired: When the reservation expired before the reserved token was used for a session Cancelled: When the reservation was cancelled manually UsedToStartCharging: When the reservation ended because the reserved token was used for a session + GlobalReservationRequirementDropped: When the reservation ended for that specific EVSE because there is a + connector free and there are less reservations than available evse's + (reservation is still there but it is not occupying this EVSE anymore). type: string enum: - Expired - Cancelled - UsedToStartCharging + - GlobalReservationRequirementDropped ReservationEnd: description: Details on Reservation End type: object @@ -63,3 +78,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 diff --git a/types/temperature.yaml b/types/temperature.yaml index 9c7cc06d9..240cabb27 100644 --- a/types/temperature.yaml +++ b/types/temperature.yaml @@ -13,5 +13,6 @@ types: identification: description: A (vendor specific) ID if required type: string - - + location: + description: Location of the measurement + type: string