diff --git a/.ci/build-kit/docker/Dockerfile b/.ci/build-kit/docker/Dockerfile new file mode 100644 index 000000000..ca655b11d --- /dev/null +++ b/.ci/build-kit/docker/Dockerfile @@ -0,0 +1,3 @@ +# syntax=docker/dockerfile:1 +ARG BASE_IMAGE_TAG=latest +FROM ghcr.io/everest/everest-ci/build-kit-base:${BASE_IMAGE_TAG} diff --git a/.ci/build-kit/install_and_test.sh b/.ci/build-kit/install_and_test.sh deleted file mode 100755 index e840ab877..000000000 --- a/.ci/build-kit/install_and_test.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -set -e - -cmake \ - -B build \ - -S "$EXT_MOUNT/source" \ - -G Ninja \ - -DBUILD_TESTING=ON \ - -DLIBOCPP16_BUILD_EXAMPLES=ON \ - -DCMAKE_BUILD_TYPE=Debug \ - -DCMAKE_INSTALL_PREFIX="$WORKSPACE_PATH/dist" \ - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON - -ninja -j$(nproc) -C build install - -trap "cp build/Testing/Temporary/LastTest.log /ext/ctest-report" EXIT - -CTEST_OUTPUT_ON_FAILURE=1 ninja -j$(nproc) -C build test - -# cmake -B build -G Ninja -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="./dist" -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ No newline at end of file diff --git a/.ci/build-kit/scripts/compile.sh b/.ci/build-kit/scripts/compile.sh new file mode 100755 index 000000000..2dd8870d5 --- /dev/null +++ b/.ci/build-kit/scripts/compile.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +cmake \ + -B "$EXT_MOUNT/build" \ + -S "$EXT_MOUNT/source" \ + -G Ninja \ + -DBUILD_TESTING=ON \ + -DLIBOCPP16_BUILD_EXAMPLES=ON \ + -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_INSTALL_PREFIX="$EXT_MOUNT/dist" \ + -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + +retVal=$? +if [ $retVal -ne 0 ]; then + echo "Configuring failed with return code $retVal" + exit $retVal +fi + +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/install.sh b/.ci/build-kit/scripts/install.sh new file mode 100755 index 000000000..174dbc773 --- /dev/null +++ b/.ci/build-kit/scripts/install.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" install +retVal=$? + +if [ $retVal -ne 0 ]; then + echo "Installation failed with return code $retVal" + exit $retVal +fi diff --git a/.ci/build-kit/scripts/run_coverage.sh b/.ci/build-kit/scripts/run_coverage.sh new file mode 100755 index 000000000..4bf1e88b4 --- /dev/null +++ b/.ci/build-kit/scripts/run_coverage.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +ninja \ + -C "$EXT_MOUNT/build" \ + ocpp_gcovr_coverage +retValHTML=$? + +ninja \ + -C "$EXT_MOUNT/build" \ + ocpp_gcovr_coverage_xml +retValXML=$? + +# Copy the generated coverage report and xml to the mounted directory in any case +cp -R "$EXT_MOUNT/build/ocpp_gcovr_coverage" "$EXT_MOUNT/gcovr-coverage" +cp "$EXT_MOUNT/build/ocpp_gcovr_coverage_xml.xml" "$EXT_MOUNT/gcovr-coverage-xml.xml" + +if [ $retValHTML -ne 0 ]; then + echo "Coverage HTML report failed with return code $retValHTML" + exit $retValHTML +fi + +if [ $retValXML -ne 0 ]; then + echo "Coverage XML report failed with return code $retValXML" + exit $retValXML +fi diff --git a/.ci/build-kit/scripts/run_unit_tests.sh b/.ci/build-kit/scripts/run_unit_tests.sh new file mode 100755 index 000000000..5fa187064 --- /dev/null +++ b/.ci/build-kit/scripts/run_unit_tests.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +ninja -C "$EXT_MOUNT/build" test +retVal=$? + +# Copy the LastTest.log file to the mounted directory in any case +cp "$EXT_MOUNT/build/Testing/Temporary/LastTest.log" "$EXT_MOUNT/ctest-report" + +if [ $retVal -ne 0 ]; then + echo "Unit tests failed with return code $retVal" + exit $retVal +fi diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 295061e17..415b94e1f 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -1,5 +1,5 @@ -name: Build and test libocpp -on: +name: Build, Lint and Test +on: pull_request: {} workflow_dispatch: inputs: @@ -11,47 +11,24 @@ on: options: - 'ubuntu-22.04' - 'large-ubuntu-22.04-xxl' -jobs: - lint: - name: Lint - runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} - steps: - - name: Checkout libocpp - uses: actions/checkout@v3 - with: - path: source - - name: Run clang-format - uses: everest/everest-ci/github-actions/run-clang-format@v1.1.0 - with: - source-dir: source - extensions: hpp,cpp - exclude: cache - install_and_test: - name: Install and test - runs-on: ${{ inputs.runner || 'ubuntu-22.04' }} - steps: - - name: Checkout libocpp - uses: actions/checkout@v3 - with: - path: source - - name: Setup run scripts - run: | - mkdir scripts - rsync -a source/.ci/build-kit/ scripts - - name: Pull docker container - run: | - docker pull --platform=linux/x86_64 --quiet ghcr.io/everest/everest-ci/build-kit-base:latest - docker image tag ghcr.io/everest/everest-ci/build-kit-base:latest build-kit - - name: Run install with tests - run: | - docker run \ - --volume "$(pwd):/ext" \ - --name test-container \ - build-kit run-script install_and_test - - name: Archive test results - if: always() - uses: actions/upload-artifact@v3 - with: - name: ctest-report - path: ${{ github.workspace }}/ctest-report + schedule: + - cron: '37 13,1 * * *' +jobs: + ci: + name: Build, Lint and Test + uses: everest/everest-ci/.github/workflows/continuous_integration.yml@v1.4.2 + permissions: + contents: read + secrets: + coverage_deploy_token: ${{ secrets.SA_GITHUB_PAT }} + with: + runner: ${{ inputs.runner || 'ubuntu-22.04' }} + artifact_deploy_target_repo: EVerest/everest.github.io + run_coverage: true + do_not_run_coverage_badge_creation: false + run_install_wheels: false + run_integration_tests: false + ctest_report_path: ctest-report + coverage_report_path: gcovr-coverage + coverage_xml_path: gcovr-coverage-xml.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e2471129..1dd244a7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.14) project(ocpp - VERSION 0.18.0 + VERSION 0.20.0 DESCRIPTION "A C++ implementation of the Open Charge Point Protocol" LANGUAGES CXX ) @@ -31,6 +31,9 @@ endif() if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING) set(LIBOCPP_BUILD_TESTING ON) + evc_include(CodeCoverage) + + append_coverage_compiler_flags() endif() # dependencies @@ -129,7 +132,7 @@ find_package(Doxygen) if(DOXYGEN_FOUND) set(DOXYGEN_OUTPUT_DIRECTORY dist/docs) set(DOXYGEN_USE_MDFILE_AS_MAINPAGE README.md) - doxygen_add_docs(doxygen-${PROJECT_NAME} README.md include lib src) + doxygen_add_docs(doxygen-${PROJECT_NAME} README.md include lib src doc) else() message("Doxygen is needed to generate documentation") endif() diff --git a/README.md b/README.md index 29e75e48f..831603012 100644 --- a/README.md +++ b/README.md @@ -1,566 +1,132 @@ # C++ implementation of OCPP + ![Github Actions](https://github.com/EVerest/libocpp/actions/workflows/build_and_test.yaml/badge.svg) -This is a C++ library implementation of OCPP for version 1.6 and 2.0.1 +This is a C++ library implementation of OCPP for version 1.6, 2.0.1 and 2.1. (see [OCPP protocols at OCA website](https://openchargealliance.org/protocols/open-charge-point-protocol/)). +The OCPP2.0.1 implementation of libocpp has been certified by the OCA for multiple hardware platforms. -It enables charging stations to communicate with cloud backends for remote -control, monitoring and billing of charging processes. - -Libocpp can be used for the communication of one charging station and multiple EVSE using a single websocket connection. - -Libocpp provides a complete implementation of OCPP 1.6. The implementation of OCPP 2.0.1 is currently under development. +Libocpp's approach to implementing the OCPP protocol is to address as much functional requirements as possible as part of the library. +Since OCPP is a protocol that affects, controls, and monitors many areas of a charging station's operation this library needs to be +integrated with your charging station firmware. -## Get Involved - -See the [COMMUNITY.md](https://github.com/EVerest/EVerest/blob/main/COMMUNITY.md) and [CONTRIBUTING.md](https://github.com/EVerest/EVerest/blob/main/CONTRIBUTING.md) of the EVerest project to get involved. - -## Table of contents -- [C++ implementation of OCPP](#c-implementation-of-ocpp) - - [Table of contents](#table-of-contents) - - [OCPP 1.6 Support](#feature-support) - - [Feature Profile Support OCPP 1.6](#feature-profile-support-ocpp-16) - - [Support for OCPP 2.0.1](#support-for-ocpp-201) - - [Feature Profile Support OCPP 2.0.1](#feature-profile-support-ocpp-201) - - [CSMS Compatibility](#csms-compatibility) - - [CSMS Compatibility OCPP 1.6](#csms-compatibility-ocpp-16) - - [CSMS Compatibility OCPP 2.0.1](#csms-compatibility-ocpp-201) - - [Integration with EVerest](#integration-with-everest) - - [Run OCPP1.6 with EVerest](#run-ocpp16-with-everest) - - [Run OCPP2.0.1 with EVerest](#run-ocpp201-with-everest) - - [Integrate this library with your Charging Station Implementation for OCPP1.6](#integrate-this-library-with-your-charging-station-implementation-for-ocpp16) - - [Overview of the required callbacks and events and what libocpp expects to happen](#overview-of-the-required-callbacks-and-events-and-what-libocpp-expects-to-happen) - - [ChargePoint() constructor](#chargepoint-constructor) - - [registering callbacks](#registering-callbacks) - - [Functions that need to be triggered from the outside after new information is availble (on\_... functions in the charge point API)](#functions-that-need-to-be-triggered-from-the-outside-after-new-information-is-availble-on_-functions-in-the-charge-point-api) - - [The following functions are triggered depending on different so called "Session Events" from the EvseManager](#the-following-functions-are-triggered-depending-on-different-so-called-session-events-from-the-evsemanager) - - [Authorization](#authorization) - - [Integrate this library with your Charging Station Implementation for OCPP2.0.1](#integrate-this-library-with-your-charging-station-implementation-for-ocpp201) - - [Register event callbacks and on\_handlers](#register-event-callbacks-and-on_handlers) - - [Initialize the database](#initialize-the-database) - - [Install libocpp](#install-libocpp) - - [Quickstart for OCPP 1.6](#quickstart-for-ocpp-16) - - [Building the doxygen documentation](#building-the-doxygen-documentation) - - [Unit testing](#unit-testing) - - [Building with FetchContent instead of EDM](#building-with-fetchcontent-instead-of-edm) - - [Support for security profile 2 and 3 with TPM in OCPP 1.6 using libwebsockets](#support-for-security-profile-2-and-3-with-tpm-in-ocpp-16-using-libwebsockets) - -## OCPP 1.6 Support +## Integration with EVerest -The following tables show the current support for the listed OCPP 1.6 feature profiles / functional blocks and application notes. +This library is integrated within the [OCPP](https://github.com/EVerest/everest-core/tree/main/modules/OCPP) and [OCPP201](https://github.com/EVerest/everest-core/tree/main/modules/OCPP201) +module within [everest-core](https://github.com/EVerest/everest-core) - the complete software stack for your charging station. It is recommended to use EVerest together with this OCPP implementation. -All documentation and the issue tracking can be found in our main repository here: https://github.com/EVerest/ +## Getting Started -### Feature Profile Support OCPP 1.6 +Check out the [Getting Started guide](doc/common/getting_started.md). It should be you starting point if you want to integrate this library with your charging station firmware. -| Feature Profile | Supported | -| -------------------------- | ------------------------- | -| Core | :heavy_check_mark: yes | -| Firmware Management | :heavy_check_mark: yes | -| Local Auth List Management | :heavy_check_mark: yes | -| Reservation | :heavy_check_mark: yes | -| Smart Charging | :heavy_check_mark: yes | -| Remote Trigger | :heavy_check_mark: yes | +## Get Involved -| Whitepapers & Application Notes | Supported | -| ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| [OCPP 1.6 Security Whitepaper (3rd edition)](https://www.openchargealliance.org/uploads/files/OCPP-1.6-security-whitepaper-edition-3.zip) | :heavy_check_mark: yes | -| [Using ISO 15118 Plug & Charge with OCPP 1.6](https://www.openchargealliance.org/uploads/files/ocpp_1_6_ISO_15118_v10.pdf) | :heavy_check_mark: yes | -| [OCPP & California Pricing Requirements](https://www.openchargealliance.org/uploads/files/ocpp_and_dms_evse_regulation-v2.0.pdf) | :heavy_check_mark: yes | +See the [COMMUNITY.md](https://github.com/EVerest/EVerest/blob/main/COMMUNITY.md) and [CONTRIBUTING.md](https://github.com/EVerest/EVerest/blob/main/CONTRIBUTING.md) of the EVerest project to get involved. -## Support for OCPP 2.0.1 +## OCPP1.6 -The development of OCPP2.0.1 is in progress. -[Current implementation status.](/doc/ocpp_201_status.md) +### Supported Feature Profiles -### Feature Profile Support OCPP 2.0.1 +OCPP1.6 is fully implemented. | Feature Profile | Supported | | -------------------------- | ------------------------- | -| Core | :heavy_check_mark: yes | -| Advanced Security | WIP | -| Local Auth List Management | | -| Smart Charging | WIP | -| Advanced Device Management | | -| Advanced User Interface | | -| Reservation | | -| ISO 15118 support | WIP | +| Core | ✅ yes | +| Firmware Management | ✅ yes | +| Local Auth List Management | ✅ yes | +| Reservation | ✅ yes | +| Smart Charging | ✅ yes | +| Remote Trigger | ✅ yes | | Whitepapers & Application Notes | Supported | | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -| [OCPP & California Pricing Requirements](https://www.openchargealliance.org/uploads/files/ocpp_and_dms_evse_regulation-v2.0.pdf) | WIP | - +| [OCPP 1.6 Security Whitepaper (3rd edition)](https://openchargealliance.org/wp-content/uploads/2023/11/OCPP-1.6-security-whitepaper-edition-3-2.zip) | ✅ yes | +| [Using ISO 15118 Plug & Charge with OCPP 1.6](https://openchargealliance.org/wp-content/uploads/2023/11/ocpp_1_6_ISO_15118_v10.pdf) | ✅ yes | +| [OCPP & California Pricing Requirements](https://openchargealliance.org/wp-content/uploads/2024/09/ocpp_and_dms_evse_regulation-v3.1.pdf) | ✅ yes | - -## CSMS Compatibility - -### CSMS Compatibility OCPP 1.6 +### CSMS Compatibility The EVerest implementation of OCPP 1.6 has been tested against the OCPP Compliance Test Tool (OCTT and OCTT2) during the implementation. -The following table shows CSMS with which this library was tested. -If you provide a CSMS that is not yet listed here, feel free to -[contact us](https://lists.lfenergy.org/g/everest)! +The following table shows the known CSMS with which this library was tested. + +- chargecloud +- chargeIQ +- Chargetic +- Compleo +- Current +- Daimler Truck +- ev.energy +- eDRV +- Fastned +- [Open Charging Cloud (GraphDefined)](https://github.com/OpenChargingCloud/WWCP_OCPP) +- Electrip Global +- EnergyStacks +- EV-Meter +- Fraunhofer IAO (ubstack CHARGE) +- Green Motion +- gridundco +- ihomer (Infuse CPMS) +- iLumen +- JibeCompany (CharlieV CMS and Chargebroker proxy) +- MSI +- PUMP (PUMP Connect) +- Scoptvision (Scopt Powerconnect) +- Siemens +- [SteVe](https://github.com/steve-community/steve) +- Syntech +- Trialog +- ubitricity +- Weev Energy + +## OCPP2.0.1 + +### Supported Functional Blocks + +| Feature Profile | Supported | +| -------------------------------------| ------------------------- | +| A. Security | ✅ yes | +| B. Provisioning | ✅ yes | +| C. Authorization | ✅ yes | +| D. LocalAuthorizationList Management | ✅ yes | +| E. Transactions | ✅ yes | +| F. RemoteControl | ✅ yes | +| G. Availability | ✅ yes | +| H. Reservation | WIP | +| I. TariffAndCost | ✅ yes | +| J. MeterValues | ✅ yes | +| K. SmartCharging | WIP | +| L. FirmwareManagement | ✅ yes | +| M. ISO 15118 CertificateManagement | ✅ yes | +| N. Diagnostics | ✅ yes | +| O. DisplayMessage | ✅ yes | +| P. DataTransfer | ✅ yes | + +The development of OCPP2.0.1 is in progress. Check the [detailed current implementation status.](doc/v201/ocpp_201_status.md). -- chargecloud -- chargeIQ -- Chargetic -- Compleo -- Current -- Daimler Truck -- ev.energy -- eDRV -- Fastned -- [Open Charging Cloud (GraphDefined)](https://github.com/OpenChargingCloud/WWCP_OCPP) -- Electrip Global -- EnergyStacks -- EV-Meter -- Fraunhofer IAO (ubstack CHARGE) -- Green Motion -- gridundco -- ihomer (Infuse CPMS) -- iLumen -- JibeCompany (CharlieV CMS and Chargebroker proxy) -- MSI -- PUMP (PUMP Connect) -- Scoptvision (Scopt Powerconnect) -- Siemens -- [SteVe](https://github.com/steve-community/steve) -- Syntech -- Trialog -- ubitricity -- Weev Energy +| Whitepapers & Application Notes | Supported | +| ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| [OCPP & California Pricing Requirements](https://openchargealliance.org/wp-content/uploads/2024/09/ocpp_and_dms_evse_regulation-v3.1.pdf) | ✅ yes | ### CSMS Compatibility OCPP 2.0.1 The current, ongoing implementation of OCPP 2.0.1 has been tested against a few CSMS and is continuously tested against OCTT2. -Additionally, the implementation has been tested against those CSMS: - -- [CitrineOS](https://lfenergy.org/projects/citrineos/) -- Chargepoint -- Current -- ihomer (Infuse CPMS) -- Instituto Tecnológico de la Energía (ITE) -- [MaEVe (Thoughtworks)](https://github.com/thoughtworks/maeve-csms) -- [Monta](https://monta.com) -- [Open Charging Cloud (GraphDefined)](https://github.com/OpenChargingCloud/WWCP_OCPP) -- Switch EV -- SWTCH - -## Integration with EVerest - -This library is automatically integrated as the OCPP and OCPP201 module within [everest-core](https://github.com/EVerest/everest-core) - the complete software stack for your charging station. It is recommended to use EVerest together with this OCPP implementation. - -### Run OCPP1.6 with EVerest -If you run libocpp with OCPP1.6 with EVerest, the build process of [everest-core](https://github.com/EVerest/everest-core) will take care of installing all necessary dependencies for you. - -### Run OCPP2.0.1 with EVerest -If you run libocpp with OCPP1.6 with EVerest, the build process of [everest-core](https://github.com/EVerest/everest-core) will take care of installing all necessary dependencies for you. This includes the initialization of the device model database using the [component config](config/v201/component_config) files. - -## Integrate this library with your Charging Station Implementation for OCPP1.6 - -OCPP is a protocol that affects, controls and monitors many areas of a charging station's operation. - -If you want to integrate this library with your charging station implementation, you have to register a couple of **callbacks** and integrate **event handlers**. This is necessary for the library to interact with your charging station according to the requirements of OCPP. - -Libocpp needs registered **callbacks** in order to execute control commands defined within OCPP (e.g Reset.req or RemoteStartTransaction.req) - -The implementation must call **event handlers** of libocpp so that the library can track the state of the charging station and trigger OCPP messages accordingly (e.g. MeterValues.req , StatusNotification.req) - -Your reference within libocpp to interact is a single instance to the class [ChargePoint](include/ocpp/v16/charge_point.hpp) for OCPP 1.6 or to the class [ChargePoint](include/ocpp/v201/charge_point.hpp) for OCPP 2.0.1. - -### Overview of the required callbacks and events and what libocpp expects to happen - -The following section will give you a high level overview of how to integrate libocpp with your application. Please use the [Doxygen Documentation](#building-the-doxygen-documentation) as an additional source for the ChargePoint API. - -In EVerest the OCPP module leverages several other modules to perform tasks that relate to authorization, reservations, charging session handling and system tasks like rebooting or firmware updates. -- Auth orchestrates authorization, utilizing different token providers like RFID reads and token validators. Libocpp mainly acts as a token validator, but in the case of RemoteStartTransactions it acts as a token provider as well -- EvseManager manages the charging session and charging state machine by communicating with a "board support package", a driver for the charging hardware that abstracts away the control pilot, relay control, power meters, etc. The EvseManager also handles reservations. -- System handles firmware updates, log uploads and resets - -The following sections explain the steps you can follow to implement their functionality on your own and integrate the libocpp directly into your charging station software without relying on EVerest. However, in most cases it's much easier to write an EVerest driver using the *everest-core/interfaces/board_support_AC.yaml* interface. - -#### ChargePoint() constructor -The main entrypoint for libocpp for OCPP1.6 is the ocpp::v16::ChargePoint constructor. -This is defined in libocpp/include/ocpp/v16/charge_point.hpp and takes the following parameters: -- config: a std::string that contains the libocpp 1.6 config. There are example configs that work with a [SteVe](https://github.com/steve-community/steve) installation [running in Docker](https://github.com/EVerest/everest-utils/tree/main/docker/steve), for example: [config-docker.json](config/v16/config-docker.json) - -- share_path: a std::filesystem path containing the path to the OCPP modules folder, for example pointing to */usr/share/everest/modules/OCPP*. This path contains the following files and directories and is installed by the libocpp install target: - ```bash - . - ├── config-docker.json - ├── config-docker-tls.json - ├── config.json - ├── init.sql - ├── logging.ini - └── profile_schemas - ├── Config.json - ├── Core.json - ├── FirmwareManagement.json - ├── Internal.json - ├── LocalAuthListManagement.json - ├── PnC.json - ├── Reservation.json - ├── Security.json - ├── SmartCharging.json - └── Custom.json - ``` - Here you can find: - - the aforementioned config files - - - a *logging.ini* that is needed to initialize logging with Everest::Logging::init(path_to_logging_ini, "name_of_binary") - - - a *init.sql* file which contains the database schema used by libocpp for its sqlite database - - - and a *profile_schemas* directory. This contains json schema files that are used to validate the libocpp config. The schemas are split up according to the OCPP1.6 feature profiles like Core, FirmwareManagement and so on. Additionally there is a schema for "Internal" configuration options (for example the ChargePointId, or CentralSystemURI). A "PnC" schema for the ISO 15118 Plug & Charge with OCPP 1.6 Application note, a "Security" schema for the OCPP 1.6 Security Whitepaper (3rd edition) and an exemplary "Custom" schema are provided as well. The Custom.json could be modified to be able to add custom configuration keys. Finally there's a Config.json schema that ties everything together - -- user_config_path: this points to a "user config", which we call a configuration file that's merged with the config that's provided in the "config" parameter. Here you can add, remove and overwrite settings without modifying the config passed in the first parameter directly. This is also used by libocpp to persistently modify config entries that are changed by the CSMS that should persist across restarts. - -- database_path: this points to the location of the sqlite database that libocpp uses to keep track of connector availability, the authorization cache and auth list, charging profiles and transaction data - -- sql_init_path: this points to the aforementioned init.sql file which contains the database schema used by libocpp for its sqlite database - -- message_log_path: this points to the directory in which libocpp can put OCPP communication logfiles for debugging purposes. This behavior can be controlled by the "LogMessages" (set to true by default) and "LogMessagesFormat" (set to ["log", "html", "session_logging"] by default, "console" and "console_detailed" are also available) configuration keys in the "Internal" section of the config file. Please note that this is intended for debugging purposes only as it logs all communication, including authentication messages. - -- evse_security: this is a pointer to an implementation of the [evse_security](include/ocpp/common/evse_security.hpp) interface. This allows you to include your custom implementation of the security related operations according to this interface. If you set this value to nullptr, the internal implementation of the security related operations of libocpp will be used. In this case you need to specify the parameter security_configuration - -- security_configuration: this parameter should only be set in case the evse_security parameter is nullptr. It specifies the file paths that are required to set up the internal evse_security implementation. Note that you need to specify bundle files for the CA certificates and directories for the certificates and keys - - The directory layout expected is as follows - ```bash - . - ├── ca - │ ├── csms - │ │ └── CSMS_ROOT_CA.pem - │ ├── cso - │ │ ├── CPO_CERT_CHAIN.pem - │ │ ├── CPO_SUB_CA1_LEAF.der - │ │ ├── CPO_SUB_CA1.pem - │ │ ├── CPO_SUB_CA2_LEAF.der - │ │ └── CPO_SUB_CA2.pem - │ ├── mf - │ │ └── MF_ROOT_CA.pem - │ ├── mo - │ │ ├── INTERMEDIATE_MO_CA_CERTS.pem - │ │ ├── MO_ROOT_CA.der - │ │ ├── MO_ROOT_CA.pem - │ │ ├── MO_SUB_CA1.der - │ │ ├── MO_SUB_CA1.pem - │ │ ├── MO_SUB_CA2.der - │ │ └── MO_SUB_CA2.pem - │ └── v2g - │ ├── V2G_ROOT_CA.der - │ └── V2G_ROOT_CA.pem - ├── client - │ ├── csms - │ │ ├── CPO_CERT_CHAIN.pem - │ │ ├── CPO_SUB_CA1.key - │ │ ├── CPO_SUB_CA2.key - │ │ ├── SECC_LEAF.der - │ │ ├── SECC_LEAF.key - │ │ └── SECC_LEAF.pem - │ ├── cso - │ │ ├── CPO_CERT_CHAIN.pem - │ │ ├── CPO_SUB_CA1.key - │ │ ├── CPO_SUB_CA2.key - │ │ ├── SECC_LEAF.der - │ │ ├── SECC_LEAF.key - │ │ └── SECC_LEAF.pem - │ └── v2g - │ └── V2G_ROOT_CA.key - ``` - -#### registering callbacks -You can (and in many cases MUST) register a number of callbacks so libocpp can interact with the charger. In EVerest most of this functionality is orchestrated by the "EvseManager" module, but you can also register your own callbacks interacting directly with your chargers software. Following is a list of callbacks that you must register and a few words about their purpose. - -TODO: in a future version of libocpp the callbacks will be organised in a struct with optional members emphasizing the required and optional callbacks. - -Some general notes: the "connector" parameter of some of the callbacks refers to the connector number as understood in the OCPP 1.6 specification, "0" means the whole charging station, the connectors with EVSEs used for charging cars start at "1". - -- register_pause_charging_callback - - this callback is used by libocpp to request pausing of charging, the "connector" parameter tells you which connector/EVSE has to pause charging - -- register_resume_charging_callback - - this callback is used by libocpp the request resuming of charging, the "connector" parameter tells you which connector/EVSE can resume charging - -- register_stop_transaction_callback - - in EVerest this calls the EvseManagers stop_transaction command which "Stops transactions and cancels charging externally, charging can only be resumed by replugging car. EVSE will also stop transaction automatically e.g. on disconnect, so this only needs to be called if the transaction should end before." - this will then signal the following events: - - ChargingFinished - - TransactionFinished - -- register_unlock_connector_callback - - can be used by libocpp to force unlock a connector - -- register_reserve_now_callback - - libocpp can use this to reserve a connector, reservation handling is outsourced to a reservation manager in EVerest that implements the reservation interface (everest-core/interfaces/reservation.yaml) - -- register_upload_diagnostics_callback - - uses a function (in EVerest provided by the System module) to upload the requested diagnostics file - -- register_upload_logs_callback - - uses a function (in EVerest provided by the System module) to upload the requested log file - -- register_update_firmware_callback - - uses a function (in EVerest provided by the System module) to perform a firmware update - -- register_signed_update_firmware_callback - - uses a function (in EVerest provided by the System module) to perform a signed firmware update - -- register_provide_token_callback - - this callback is used in a remote start transaction to provide a token (prevalidated or not) to the authorization system - -- register_set_connection_timeout_callback - - used by libocpp to set the authorization or plug in connection timeout in the authorization system based on the "ConnectionTimeout" configuration key - -- register_disable_evse_callback - - used to disable the EVSE (ChangeAvailability.req) - -- register_enable_evse_callback - - used to enable the EVSE (ChangeAvailability.req) - -- register_cancel_reservation_callback - - used to cancel a reservation in the reservation manager (CancelReservation.req) - -- register_signal_set_charging_profiles_callback - - used to signal that new charging schedule(s) have been set, you can then use - get_all_composite_charging_schedules(duration_s) to get the new valid charging schedules - -- register_is_reset_allowed_callback - - used to inquire (in EVerest from the System module) if a reset is allowed - -- register_reset_callback - - used to perform a reset of the requested type - -- register_connection_state_changed_callback - - used to inform about the connection state to the CSMS (connected = true, disconnected = false) - -- register_configuration_key_changed_callback - used to react on a changed configuration key. This callback is called when the specified configuration key has been changed by the CSMS - - -#### Functions that need to be triggered from the outside after new information is availble (on_... functions in the charge point API) -- on_log_status_notification(int32_t request_id, std::string log_status) - - can be used to notify libocpp of a log status notification - -- on_firmware_update_status_notification(int32_t request_id, std::string firmware_update_status) - - can be used to notify libocpp of a firmware update status notification - -- on_meter_values(int32_t connector, const Powermeter& powermeter) - - provides a Powermeter struct to libocpp (for sending meter values during charging sessions or periodically) - -- on_max_current_offered(int32_t connector, int32_t max_current) - - the maximum current offered to the EV on this connector (in ampere) - -#### The following functions are triggered depending on different so called "Session Events" from the EvseManager -each of these functions will have a small note what the Session Event was and what it triggers in libocpp - -- on_enabled(int32_t connector) - - Notifies libocpp that the connector is functional and operational - -- on_disabled(int32_t connector) - - Notifies libocpp that the connector is disabled - -- on_transaction_started - - Notifies libocpp that a transaction at the given connector has started, this means that authorization is available and the car is plugged in. - - Some of its parameters: - - session_id is an internal session_id originating in the EvseManager to keep track of the transaction, this is NOT to be mistaken for the transactionId from the StartTransactionResponse in OCPP! - - id_token is the token with which the transaction was authenticated - - meter_start contains the meter value in Wh for the connector at start of the transaction - - timestamp at the start of the transaction - -- on_transaction_stopped - - Notifies libocpp that the transaction on the given connector with the given reason has been stopped. - - Some of its parameters: - - timestamp at the end of the transaction - - energy_wh_import contains the meter value in Wh for the connector at end of the transaction - -- on_suspend_charging_ev - - Notifies libocpp that the EV has paused charging - -- on_suspend_charging_evse - - Notifies libocpp that the EVSE has paused charging - -- on_resume_charging - - Notifies libocpp that charging has resumed - -- on_session_started - - this is mostly used for logging and changing the connector state - -- on_session_stopped - - this is mostly used for logging and changing the connector state - -- on_error - - Notify libocpp of an error - -- on_reservation_start - - Notifies libocpp that a reservation has started - -- on_reservation_end - - Notifies libocpp that a reservation has ended - -#### Authorization -In EVerest authorization is handled by the Auth module and various auth token providers and validators. The OCPP module acts as both a token provider (for pre validated tokens in RemoteStartTransactions) and a token validator (using the authorize requests, or plug & charge) -To use libocpp as a auth token validator (e.g. before starting a transaction) you can call the "authorize_id_token" function of the ChargePoint object - -## Integrate this library with your Charging Station Implementation for OCPP2.0.1 -TODO - -### Register event callbacks and on_handlers - -### Initialize the database -- Use provided sql database or implement your own storage drive - - - -## Install libocpp - -For Debian GNU/Linux 11 you will need the following dependencies: - -```bash - sudo apt install build-essential cmake python3-pip libboost-all-dev libsqlite3-dev libssl-dev -``` - -OpenSSL version 3.0 or above is required. - -Clone this repository. - -```bash - git clone https://github.com/EVerest/libocpp -``` - -In the libocpp folder create a folder named build and cd into it. -Execute cmake and then make install: - -```bash - mkdir build && cd build - cmake .. - make install -``` - -## Quickstart for OCPP 1.6 - -Libocpp provides a small standalone OCPP1.6 client that you can control using command line. - -Install the dependencies and libocpp as described in [Install libocpp](#install-libocpp). - -Make sure you modify the following config entries in the [config.json](config/v16/config.json) file according to the CSMS you want to connect to before executing make install. - -```json -{ - "Internal": { - "ChargePointId": "", - "CentralSystemURI": "" - } -} -``` - -Change into libocpp/build and execute cmake and then make install: - -```bash - cd build - cmake -DLIBOCPP16_BUILD_EXAMPLES=ON -DCMAKE_INSTALL_PREFIX=./dist .. - make -j$(nproc) install -``` - -Use the following command to start the charge point. Replace the config with [config-docker.json](config/v16/config-docker.json) if you want to test with the [SteVe](https://github.com/steve-community/steve#docker) CSMS running in a docker container. - -```bash - ./dist/bin/charge_point \ - --maindir ./dist \ - --conf config.json -``` - -Type `help` to see a list of possible commands. - -## Building the doxygen documentation -```bash - cmake -S . -B build - cmake --build build --target doxygen-ocpp -``` -You will find the generated doxygen documentation at: -`build/dist/docs/html/index.html` - -The main reference for the integration of libocpp for OCPP1.6 is the ocpp::v16::ChargePoint class defined in libocpp/include/ocpp/v16/charge_point.hpp . - - -## Unit testing - -GTest is required for building the test cases target. -To build the target and run the tests you can reference the script `.ci/build-kit/install_and_test.sh`. -The script allows the GitHub Actions runner to execute. - -Local testing: -```bash -mkdir build -cmake -B build -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="./dist" -cd build -make -j$(nproc) install -``` -Run any required tests from build/tests. - -## Building with FetchContent instead of EDM -In [doc/build-with-fetchcontent](doc/build-with-fetchcontent) you can find an example how to build libocpp with FetchContent instead of EDM. - -### Support for TPM keys - -In order to use the TPM keys, it is mandatory to use the default libwebsocket implementation. - -## Support for websocket++ - -The old websocket++ implementation has been removed. Enabling websocket++ will prevent compilation from starting. +Additionally, the implementation has been tested against these CSMS: -### Support for iface +- [CitrineOS](https://lfenergy.org/projects/citrineos/) +- Chargepoint +- Current +- ihomer (Infuse CPMS) +- Instituto Tecnológico de la Energía (ITE) +- [MaEVe (Thoughtworks)](https://github.com/thoughtworks/maeve-csms) +- [Monta](https://monta.com) +- [Open Charging Cloud (GraphDefined)](https://github.com/OpenChargingCloud/WWCP_OCPP) +- Switch EV +- SWTCH -In order to connect through a custom network iface, a custom internal config variable 'IFace' can be used. +## OCPP2.1 -```json -"Internal": { - "IFace" : "enp43s0" -} -``` +The implementation of OCPP2.1 is currently under development. diff --git a/config/v16/config-full.json b/config/v16/config-full.json new file mode 100644 index 000000000..4dfa8435a --- /dev/null +++ b/config/v16/config-full.json @@ -0,0 +1,180 @@ +{ + "Internal": { + "ChargePointId": "cp001", + "CentralSystemURI": "127.0.0.1:8180/steve/websocket/CentralSystemService/", + "ChargeBoxSerialNumber": "cp001", + "ChargePointModel": "Yeti", + "ChargePointSerialNumber": "cp001", + "ChargePointVendor": "Pionix", + "FirmwareVersion": "0.1", + "ICCID": "891004234814455936F", + "IMSI": "262 01 9876543210", + "MeterSerialNumber": "123-456-789", + "MeterType": "AC", + "SupportedCiphers12": [ + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "AES128-GCM-SHA256", + "AES256-GCM-SHA384" + ], + "SupportedCiphers13": [ + "TLS_AES_256_GCM_SHA384", + "TLS_AES_128_GCM_SHA256" + ], + "UseTPM": false, + "RetryBackoffRandomRange": 10, + "RetryBackoffRepeatTimes": 3, + "RetryBackoffWaitMinimum": 3, + "AuthorizeConnectorZeroOnConnectorOne": true, + "LogMessages": true, + "LogMessagesFormat": [ + "log", + "html", + "session_logging", + "security" + ], + "LogRotation": false, + "LogRotationDateSuffix": false, + "LogRotationMaximumFileSize": 0, + "LogRotationMaximumFileCount": 0, + "SupportedChargingProfilePurposeTypes": [ + "ChargePointMaxProfile", + "TxDefaultProfile", + "TxProfile" + ], + "MaxCompositeScheduleDuration": 31536000, + "WebsocketPingPayload": "Hello from EVerest!", + "WebsocketPongTimeout": 5, + "UseSslDefaultVerifyPaths": true, + "VerifyCsmsCommonName": true, + "VerifyCsmsAllowWildcards": true, + "OcspRequestInterval": 604800, + "SeccLeafSubjectCommonName": "cp001", + "SeccLeafSubjectCountry": "DE", + "SeccLeafSubjectOrganization": "Pionix", + "ConnectorEvseIds": "DE*PNX*100001,DE*PNX*100002", + "AllowChargingProfileWithoutStartSchedule": false, + "WaitForStopTransactionsOnResetTimeout": 60, + "QueueAllMessages": true, + "MessageTypesDiscardForQueueing": "Heartbeat", + "MessageQueueSizeThreshold": 5000, + "SupportedMeasurands": "Energy.Active.Import.Register,Energy.Active.Export.Register,Power.Active.Import,Voltage,Current.Import,Frequency,Current.Offered,Power.Offered,SoC", + "MaxMessageSize": 65000, + "TLSKeylogFile": "/tmp/ocpp_tls_keylog.txt", + "EnableTLSKeylog": false + }, + "Core": { + "AllowOfflineTxForUnknownId": true, + "AuthorizationCacheEnabled": true, + "AuthorizeRemoteTxRequests": true, + "BlinkRepeat": 0, + "ClockAlignedDataInterval": 900, + "ConnectionTimeOut": 120, + "ConnectorPhaseRotation": "RST0.RST,1.RST,2.RTS", + "ConnectorPhaseRotationMaxLength": 100, + "GetConfigurationMaxKeys": 1024, + "HeartbeatInterval": 900, + "LightIntensity": 0, + "LocalAuthorizeOffline": true, + "LocalPreAuthorize": true, + "MaxEnergyOnInvalidId": 500, + "MeterValuesAlignedData": "Energy.Active.Import.Register", + "MeterValuesAlignedDataMaxLength": 1024, + "MeterValuesSampledData": "Energy.Active.Import.Register", + "MeterValuesSampledDataMaxLength": 1024, + "MeterValueSampleInterval": 300, + "MinimumStatusDuration": 2, + "NumberOfConnectors": 2, + "ResetRetries": 1, + "StopTransactionOnEVSideDisconnect": true, + "StopTransactionOnInvalidId": true, + "StopTxnAlignedData": "Energy.Active.Import.Register", + "StopTxnAlignedDataMaxLength": 1024, + "StopTxnSampledData": "Energy.Active.Import.Register", + "StopTxnSampledDataMaxLength": 1024, + "SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging", + "SupportedFeatureProfilesMaxLength": 1024, + "TransactionMessageAttempts": 1, + "TransactionMessageRetryInterval": 10, + "UnlockConnectorOnEVSideDisconnect": true, + "WebsocketPingInterval": 10 + }, + "LocalAuthListManagement": { + "LocalAuthListEnabled": true, + "LocalAuthListMaxLength": 1024, + "SendLocalListMaxLength": 1024 + }, + "SmartCharging": { + "ChargeProfileMaxStackLevel": 1000, + "ChargingScheduleAllowedChargingRateUnit": "W,A", + "ChargingScheduleMaxPeriods": 1000, + "ConnectorSwitch3to1PhaseSupported": true, + "MaxChargingProfilesInstalled": 1000 + }, + "FirmwareManagement": { + "SupportedFileTransferProtocols": "FTP" + }, + "Reservation": { + "ReserveConnectorZeroSupported": false + }, + "Security": { + "AdditionalRootCertificateCheck": false, + "AuthorizationKey": "DEADBEEFDEADBEEF", + "CertificateSignedMaxChainSize": 10000, + "CertificateStoreMaxLength": 1000, + "CpoName": "Pionix", + "SecurityProfile": 1, + "DisableSecurityEventNotifications": true + }, + "PnC": { + "ISO15118PnCEnabled": true, + "CentralContractValidationAllowed": true, + "CertificateSignedMaxChainSize": 10000, + "CertSigningWaitMinimum": 30, + "CertSigningRepeatTimes": 2, + "CertificateStoreMaxLength": 1000, + "ContractValidationOffline": true + }, + "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": "02:00", + "NextTimeOffsetTransitionDateTime": "2024-01-01T00:00:00", + "TimeOffsetNextTransition": "01:00", + "CustomIdleFeeAfterStop": false, + "MultiLanguageSupportedLanguages": "en, nl, de, nb_NO", + "CustomMultiLanguageMessages": true, + "Language": "en" + } +} diff --git a/config/v16/profile_schemas/Internal.json b/config/v16/profile_schemas/Internal.json index 276aec336..f9e5aed42 100644 --- a/config/v16/profile_schemas/Internal.json +++ b/config/v16/profile_schemas/Internal.json @@ -197,6 +197,11 @@ "TxProfile" ] }, + "IgnoredProfilePurposesOffline": { + "$comment": "Allows configuration of comma seperated list of ChargingProfilePurposes that are ignored in the composite schedule caluclation when offline.", + "type": "string", + "readOnly": false + }, "MaxCompositeScheduleDuration": { "$comment": "Maximum duration in seconds of GetCompositeSchedule.req. For GetCompositeSchedule.req with a greater duration the schedule for only the MaxCompositeScheduleDuration will be calculated", "type": "integer", diff --git a/config/v201/component_config/standardized/SmartChargingCtrlr.json b/config/v201/component_config/standardized/SmartChargingCtrlr.json index ebd7d5bdc..bb268618d 100644 --- a/config/v201/component_config/standardized/SmartChargingCtrlr.json +++ b/config/v201/component_config/standardized/SmartChargingCtrlr.json @@ -253,6 +253,23 @@ "description": "Supply voltage of the grid. This value is only used in case a conversion between smart charging amp and watt limits is required" } }, + "IgnoredProfilePurposesOffline": { + "variable_name": "IgnoredProfilePurposesOffline", + "characteristics": { + "valuesList": "ChargingStationMaxProfile,TxDefaultProfile,TxProfile", + "supportsMonitoring": true, + "dataType": "MemberList" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite", + "value": "" + } + ], + "description": "Allows configuration of comma seperated list of ChargingProfilePurposes that are ignored in the composite schedule caluclation when offline.", + "type": "string" + }, "required": [ "ChargingProfileMaxStackLevel", "ChargingScheduleChargingRateUnit", diff --git a/config/v201/device_model_migrations/2_down-variable_source.sql b/config/v201/device_model_migrations/2_down-variable_source.sql new file mode 100644 index 000000000..0b21b0445 --- /dev/null +++ b/config/v201/device_model_migrations/2_down-variable_source.sql @@ -0,0 +1,2 @@ +ALTER TABLE VARIABLE +DROP COLUMN SOURCE; diff --git a/config/v201/device_model_migrations/2_up-variable_source.sql b/config/v201/device_model_migrations/2_up-variable_source.sql new file mode 100644 index 000000000..65ebf907f --- /dev/null +++ b/config/v201/device_model_migrations/2_up-variable_source.sql @@ -0,0 +1,2 @@ +ALTER TABLE VARIABLE +ADD COLUMN SOURCE TEXT; diff --git a/dependencies.yaml b/dependencies.yaml index ae64eedf0..93091d29b 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -27,7 +27,7 @@ date: options: ["BUILD_TZ_LIB ON", "HAS_REMOTE_API 0", "USE_AUTOLOAD 0", "USE_SYSTEM_TZ_DB ON"] libevse-security: git: https://github.com/EVerest/libevse-security.git - git_tag: v0.8.0 + git_tag: v0.9.1 libwebsockets: git: https://github.com/warmcat/libwebsockets.git git_tag: v4.3.3 diff --git a/doc/build-with-fetchcontent/CMakeLists.txt b/doc/common/build-with-fetchcontent/CMakeLists.txt similarity index 100% rename from doc/build-with-fetchcontent/CMakeLists.txt rename to doc/common/build-with-fetchcontent/CMakeLists.txt diff --git a/doc/common/build-with-fetchcontent/README.md b/doc/common/build-with-fetchcontent/README.md new file mode 100644 index 000000000..51f70b10f --- /dev/null +++ b/doc/common/build-with-fetchcontent/README.md @@ -0,0 +1,3 @@ +# Building with FetchContent instead of EDM + +In this directory you can find a [CMakeLists.txt](CMakeLists.txt) that serves as an example how to build libocpp with FetchContent instead of EDM. diff --git a/doc/california_pricing_requirements.md b/doc/common/california_pricing_requirements.md similarity index 82% rename from doc/california_pricing_requirements.md rename to doc/common/california_pricing_requirements.md index 78ad57fa5..c3416f8ae 100644 --- a/doc/california_pricing_requirements.md +++ b/doc/common/california_pricing_requirements.md @@ -2,35 +2,31 @@ OCPP has several whitepapers, which can be found here: https://openchargealliance.org/whitepapers/ -One of them is OCPP & California Pricing Requirements. This can be optionally enabled in libocpp, for OCPP 1.6 as well -as OCPP 2.0.1. +One of them is OCPP & California Pricing Requirements. This can be optionally enabled in libocpp, for OCPP 1.6 as well as OCPP 2.0.1. ## Callbacks in libocpp -To be kind of compatible with eachother, the callbacks for OCPP 1.6 and 2.0.1 use the same structs with the pricing +To be kind of compatible with eachother, the callbacks for OCPP 1.6 and 2.0.1 use the same structs with the pricing information. ### User-specific price / SetUserPrice The User-specific price is used for display purposes only and can be sent as soon as the user identifies itself with an -id token. It should not be used to calculate prices. -Internally, the messages in the DataTransfer json (for 1.6) is converted to a `DisplayMessage`, defined in +id token. It should not be used to calculate prices. +Internally, the messages in the DataTransfer json (for 1.6) is converted to a `DisplayMessage`, defined in `common/types.hpp`. In case of multi language messages, they are all added to the DisplayMessage vector. -If the message is sent when a transaction has already started, the session id will be included in the display message -and the `IdentifierType` will be set to `SessionId`. If it has not started yet, the id token is sent with +If the message is sent when a transaction has already started, the session id will be included in the display message and the `IdentifierType` will be set to `SessionId`. If it has not started yet, the id token is sent with `IdentifierType` set to `IdToken`. - ### Running cost and Final / total cost -The running cost and final cost messages are converted to a `RunningCost` struct, also defined in `common/types.hpp`. -The triggers in the message (running cost) are handled in libocpp itself. +The running cost and final cost messages are converted to a `RunningCost` struct, also defined in `common/types.hpp`. +The triggers in the message (running cost) are handled in libocpp itself. The prices are converted to integers, because floating point numbers are not precise enough for pricing calculations. -To set the number of decimals to calculate with, you should set NumberOfDecimalsForCostValues (1.6, in CostAndPrice / +To set the number of decimals to calculate with, you should set NumberOfDecimalsForCostValues (1.6, in CostAndPrice / 2.0.1, TariffCostCtrlr). Default is 3. There might be messages in multiple languages, they are all added to the messages vector. - ## OCPP 1.6 OCPP 1.6 mostly uses DataTransfer to send the pricing messages, and also has some extra configuration items. In libocpp, @@ -52,19 +48,16 @@ the DataTransfer message is converted to internally used structs as described ab | `NextTimeOffsetTransitionDateTime` | When to change to summer or winter time, to the offset `TimeOffsetNextTransition` | | `TimeOffsetNextTransition` | What the new offset should be at the given `NextTimeOffsetTransationDateTime` (readwrite) | - ### Callbacks For California Pricing to work, the following callbacks must be enabled: + - `session_cost_callback`, used for running cost and final cost - `set_display_message_callback`, used to show a user specific price - ## OCPP 2.0.1 -OCPP 2.0.1 uses different mechanisms to send pricing information. The messages are converted to internally used structs -as descripbed above. For California Pricing Requirements to work, DisplayMessage and TariffAndCost must be implemented -as well. +OCPP 2.0.1 uses different mechanisms to send pricing information. The messages are converted to internally used structs as descripbed above. For California Pricing Requirements to work, DisplayMessage and TariffAndCost must be implemented as well. ### Device Model Variables @@ -87,31 +80,24 @@ as well. | `TotalCostFallbackMessage` | `` | `TariffCostCtrlr` | Multi language TotalCostFallbackMessage. There must be a variable with the language as instance for every supported language. | | `Language` | | `DisplayMessageCtrlr` | Default language code (RFC 5646). The `valuesList` holds the supported languages of the charging station. The value must be one of `valuesList`. | - -> **_NOTE:_** Tariff and cost can be enabled separately. To be able to use all functionality, it is recommended to -enable both. If cost is enabled and tariff is not enabled, the total cost message will not contain the personal message -(`set_running_cost_callback`). -If tariff is enabled and cost is not enabled, the total cost message will only be a DisplayMessage +> **_NOTE:_** Tariff and cost can be enabled separately. To be able to use all functionality, it is recommended to +enable both. If cost is enabled and tariff is not enabled, the total cost message will not contain the personal message (`set_running_cost_callback`). +If tariff is enabled and cost is not enabled, the total cost message will only be a DisplayMessage (`set_display_message_callback`) containing the personal message(s). - ### Callbacks For California Pricing to work, the following callbacks must be enabled: + - `set_running_cost_callback` - `set_display_message_callback` -For the tariff information (the personal messages), the `set_display_message_callback` is used. The same callback is -also used for the SetDisplayMessageRequest in OCPP. The latter does require an id, the former will not have an id. So -when `GetDisplayMessageRequest` is called from the CSMS, the Tariff display messages (that do not have an id) should not -be returned. They should also be removed as soon as the transaction has ended. +For the tariff information (the personal messages), the `set_display_message_callback` is used. The same callback is also used for the SetDisplayMessageRequest in OCPP. The latter does require an id, the former will not have an id. So when `GetDisplayMessageRequest` is called from the CSMS, the Tariff display messages (that do not have an id) should not be returned. They should also be removed as soon as the transaction has ended. -Driver specific tariffs / pricing information can be returned by the CSMS in the `AuthorizeResponse` message. In -libocpp, the whole message is just forwared (pricing information is not extracted from it), because the pricing -information is coupled to the authorize response. So when Tariff and Cost are enabled, the `idTokenInfo` field must be -read for pricing information. +Driver specific tariffs / pricing information can be returned by the CSMS in the `AuthorizeResponse` message. In +libocpp, the whole message is just forwared (pricing information is not extracted from it), because the pricing +information is coupled to the authorize response. So when Tariff and Cost are enabled, the `idTokenInfo` field must be read for pricing information. Cost information is also sent by the CSMS in the TransactionEventResponse. In that case, the pricing / cost information -is extracted from the message and a RunningCost message is sent containing the current cost and extra messages -(optional). If only Tariff is enabled and there is a personal message in the TransationEventResponse, a DisplayMessage -is sent. +is extracted from the message and a RunningCost message is sent containing the current cost and extra messages +(optional). If only Tariff is enabled and there is a personal message in the TransationEventResponse, a DisplayMessage is sent. diff --git a/doc/database_exception_handling.md b/doc/common/database_exception_handling.md similarity index 100% rename from doc/database_exception_handling.md rename to doc/common/database_exception_handling.md diff --git a/doc/database_migrations.md b/doc/common/database_migrations.md similarity index 99% rename from doc/database_migrations.md rename to doc/common/database_migrations.md index e7a0c4412..c132f20d3 100644 --- a/doc/database_migrations.md +++ b/doc/common/database_migrations.md @@ -14,14 +14,15 @@ Using the database migrations is fairly straightforward. Most of the details are - Old databases need to be removed so a new database can be created using the migrations. This is to make sure that there is exact control over the schema of the database and no remains are present. **Requirements:** -- Minimal SQLite version 3.35.0 for `ALTER TABLE DROP COLUMN` support +- Minimal SQLite version 3.35.0 for `ALTER TABLE DROP COLUMN` support ## How to contribute changes to the database schema If changes need to be made to the database schema this can be done by adding new migration files in the relevant `config/vxx/core_migrations` folder. ### File format + The filenames must have the following format: `x_up[-description].sql` and `x_down[-description].sql` @@ -32,17 +33,17 @@ The files always exist in pairs except for the initial "up" file used to start t CMake will validate the completeness of the migration pairs and the filenames. If this is not correct CMake will fail to initialize. ### Schema changes + The schema changes should be done in such a way that the data present in the databases will persist unless it is really necessary to remove stuff. ### Unit testing + There are already unit tests present that will automatically perform a full up and down migration with all the intermediate steps in the `core_migrations` folder. The files will be auto detected for this. Additionally it is recommended to write specific unit tests for your migration that validate that the changes you have made have the expected outcome. - ## Design consideration - - We start out with an SQL init file that is used to start the database with. This file should not be changed anymore once this functionality is implemented. - Every change we want to make to the database is done through migration files. These files are simply more SQL files that can be applied to the database in the right order. - Every change should consist of an up and a down file. diff --git a/doc/common/getting_started.md b/doc/common/getting_started.md new file mode 100644 index 000000000..c14044127 --- /dev/null +++ b/doc/common/getting_started.md @@ -0,0 +1,63 @@ +# Getting Started + +## Requirements + +For Debian GNU/Linux 11 you will need the following dependencies: + +```bash + sudo apt install build-essential cmake python3-pip libboost-all-dev libsqlite3-dev libssl-dev +``` + +OpenSSL version 3.0 or above is required. + +Clone this repository. + +```bash + git clone https://github.com/EVerest/libocpp +``` + +In the libocpp folder create a folder named build and cd into it. +Execute cmake and then make: + +```bash + mkdir build && cd build + cmake .. + make -j$(nproc) +``` + +## Unit testing + +GTest is required for building the test cases target. +To build the target and run the tests you can reference the script `.ci/build-kit/install_and_test.sh`. +The script allows the GitHub Actions runner to execute. + +Local testing: + +```bash +mkdir build +cmake -B build -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="./dist" +cd build +make -j$(nproc) install +``` + +Run any required tests from build/tests. + +## Get Started with OCPP1.6 + +Please see the [Getting Started documentation for OCPP1.6](../v16/getting_started.md). + +## Get Started with OCPP2.0.1 + +Please see the [Getting Started documentation for OCPP2.0.1](../v201/getting_started.md). + +## Building the doxygen documentation + +```bash + cmake -S . -B build + cmake --build build --target doxygen-ocpp +``` + +You will find the generated doxygen documentation at: +`build/dist/docs/html/index.html` + +The main reference for the integration of libocpp for OCPP1.6 is the ocpp::v16::ChargePoint class defined in `v16/charge_point.hpp` , for OCPP2.0.1 that is the ocpp::v201::ChargePoint class defined in `v201/charge_point.hpp` . diff --git a/doc/smart_charging_profiles.md b/doc/common/smart_charging_profiles.md similarity index 97% rename from doc/smart_charging_profiles.md rename to doc/common/smart_charging_profiles.md index 349e2a779..020c89f77 100644 --- a/doc/smart_charging_profiles.md +++ b/doc/common/smart_charging_profiles.md @@ -1,9 +1,10 @@ # Notes on Smart Charging Profiles and approach + There are some complexities calculating composite schedules and this note explains the approach. The new approach is in `profile.cpp` and `profile.hpp` and then integrated into `smart_charging.cpp` maintaining the same API. -``` +```cpp std::vector get_valid_profiles( const DateTime& start_time, const DateTime& end_time, int connector_id); @@ -17,7 +18,9 @@ The new approach is in `profile.cpp` and `profile.hpp` and then integrated into ``` ## get_valid_profiles() + Retrieves all profiles that should be considered for calculating the composite schedule. + - checks that profiles are associated with the correct connector - checks that profiles match the transaction ID (when there is an active transaction) - start_time and end_time are not used in the new implementation @@ -28,9 +31,11 @@ within that period. However only the `validFrom` and `validTo` settings should b `calculate_enhanced_composite_schedule` checks the `start_time` and `end_time` so it is not essential to remove profiles at this point. ## calculate_enhanced_composite_schedule() + Assumes that profiles for other connectors and transactions have been removed. Processes profiles: + 1. obtain session/transaction start time 2. split the profiles into ChargePointMax/TxDefault/Tx sets 3. for each set calculate the composite schedule using the preferred units (Amps/Watts) @@ -40,6 +45,7 @@ Processes profiles: When combining; TxDefault is the starting limit, overridden by Tx and potentially lowered by ChargePointMax. The result will never be higher than a ChargePointMax limit (where there is one). ## Time handling + The approach removes all calls for obtaining the current date and time. - where there is a transaction then the start time of the transaction is obtained @@ -50,6 +56,7 @@ For generating a composite schedule relative schedules are included based on the The removal of any relationship to the current time simplifies writing test cases and debugging test failures. ## Default limit + The OCPP 1.6 specification doesn't support gaps in charging schedules. This presents a problem while creating a composite schedule when there is a period of time when no profile is active. - profile 1: stack level 10, Monday 10:00 for 2 hours @@ -65,7 +72,7 @@ Where the limit is not known then a default limit of `48.0 Amps` is used when ca A different default can be specified by installing a lower stack level TxDefault profile e.g. -``` +```json { "chargingProfileId": 1, "chargingProfileKind": "Relative", @@ -84,11 +91,12 @@ A different default can be specified by installing a lower stack level TxDefault ``` ## No valid profiles + Since a default limit is applied a composite schedule will always start at the `start_time` and have a fixed duration even when there are no valid profiles for that time period. e.g. for 2024-01-01 starting at 08:00 for 10 minutes -``` +```json { "status": "Accepted", "scheduleStart": "2024-01-01T08:00:00Z", @@ -108,7 +116,7 @@ When building the OCPP response "startSchedule" could be excluded however the co According to the OCPP specification section `7.13 ChargingSchedule` chargingSchedulePeriod is a required field and must have at least one entry. Hence the following is not a valid response: -``` +```json { "status": "Accepted", "scheduleStart": "2024-01-01T08:00:00Z", @@ -124,6 +132,7 @@ According to the OCPP specification section `7.13 ChargingSchedule` chargingSche ## Profile validity The following items need to be considered when looking at a schedule: + - validFrom - validTo - transaction start time @@ -133,11 +142,11 @@ The following items need to be considered when looking at a schedule: The following sections explore some interesting edge cases. - ### validFrom & validTo and transaction start time For the following schedule -``` + +```json { "chargingProfileId": 1, "chargingProfileKind": "Relative", @@ -156,7 +165,7 @@ For the following schedule } ], }, - "validFrom": "2024-01-01T12:00:00Z" + "validFrom": "2024-01-01T12:00:00Z", "validTo": "2024-01-01T20:00:00Z" } ``` @@ -168,7 +177,8 @@ The PEV plugs in at "2024-01-01T19:50:00Z" the result is charging for 10 minutes ### startSchedule and daily recurring For the following schedule -``` + +```json { "chargingProfileId": 1, "chargingProfileKind": "Recurring", @@ -199,7 +209,8 @@ The PEV plugs in at "2024-01-10T11:50:00Z" the result is charging at 6.0A for 10 ### startSchedule and daily recurring with duration For the following schedule -``` + +```json { "chargingProfileId": 1, "chargingProfileKind": "Recurring", @@ -221,7 +232,7 @@ For the following schedule } ], }, - "validFrom": "2024-02-01T12:00:00Z" + "validFrom": "2024-02-01T12:00:00Z", "validTo": "2024-03-01T09:00:00Z" } ``` @@ -230,4 +241,4 @@ The PEV plugs in at "2024-02-10T11:50:00Z" the result is based on the default li The PEV plugs in at "2024-02-01T11:50:00Z" the result is based on the default limit since the profile isn't valid yet. i.e. no charging for 10 minutes and then at 32.0A from 12:00 when the schedule starts. -The PEV plugs in at "2024-03-01T08:00:00Z" the result is charging at 6.0A (based on the previous day) and then no charging at 09:00 when the profile is no longer valid. \ No newline at end of file +The PEV plugs in at "2024-03-01T08:00:00Z" the result is charging at 6.0A (based on the previous day) and then no charging at 09:00 when the profile is no longer valid. diff --git a/doc/message_dispatching.md b/doc/message_dispatching.md new file mode 100644 index 000000000..1d6b555a9 --- /dev/null +++ b/doc/message_dispatching.md @@ -0,0 +1,51 @@ +# Message Dispatching Class Diagram + +```mermaid +classDiagram +class MessageDispatcherInterface { + +dispatch_call(const json& call, bool triggered = false) + +dispatch_call_async(const json& call, bool triggered = false): std::future~EnhancedMessage~T~~ + +dispatch_call_result(const json& call_result) + +dispatch_call_error(const json& call_error) +} + +class v16_MessageDispatcher { + - MessageQueue& message_queue + - ChargePointConfiguration& configuration + - RegistrationStatus& registration_status +} + +class v201_MessageDispatcher { + - MessageQueue& message_queue + - DeviceModel& device_model + - ConnectivityManager& connectivity_manager + - RegistrationStatusEnum& registration_status +} + +class v201_DataTransferInterface { + +data_transfer_req(request: DataTransferRequest): std::optional~DataTransferResponse~ + +handle_data_transfer_req(call: Call~DataTransferRequest~) +} + +class v201_DataTransfer { + -MessageDispatcherInterface &message_dispatcher + -std::optional~function~ data_transfer_callback +} + +class v201_ChargePoint { + std::unique_ptr~MessageDispatcherInterface~ message_dispatcher + std::unique_ptr~v201_DataTransferInterface~ data_transfer +} + +class v16_ChargePoint { + std::unique_ptr~MessageDispatcherInterface~ message_dispatcher +} + +MessageDispatcherInterface <|-- v16_MessageDispatcher +MessageDispatcherInterface <|-- v201_MessageDispatcher +v201_DataTransferInterface <|-- v201_DataTransfer +MessageDispatcherInterface *-- v201_DataTransfer +MessageDispatcherInterface *-- v201_ChargePoint +v201_DataTransferInterface *-- v201_ChargePoint +MessageDispatcherInterface *-- v16_ChargePoint +``` diff --git a/doc/v16/getting_started.md b/doc/v16/getting_started.md new file mode 100644 index 000000000..176cdd81a --- /dev/null +++ b/doc/v16/getting_started.md @@ -0,0 +1,298 @@ +# Getting Started with OCPP1.6 + +## Integrate this library with your Charging Station Implementation for OCPP1.6 + +OCPP is a protocol that affects, controls and monitors many areas of a charging station's operation. + +If you want to integrate this library with your charging station implementation, you have to register a couple of **callbacks** and integrate **event handlers**. This is necessary for the library to interact with your charging station according to the requirements of OCPP. + +Libocpp needs registered **callbacks** in order to execute control commands defined within OCPP (e.g Reset.req or RemoteStartTransaction.req) + +The implementation must call **event handlers** of libocpp so that the library can track the state of the charging station and trigger OCPP messages accordingly (e.g. MeterValues.req , StatusNotification.req) + +Your reference within libocpp to interact is a single instance to the class ocpp::v16::ChargePoint ([ChargePoint](include/ocpp/v16/charge_point.hpp)) defined in `ocpp/v16/charge_point.hpp` for OCPP 1.6. + +### Overview of the required callbacks and events and what libocpp expects to happen + +The following section will give you a high level overview of how to integrate libocpp with your application. Please use the [Doxygen Documentation](#building-the-doxygen-documentation) as an additional source for the ChargePoint API. + +In EVerest the OCPP module leverages several other modules to perform tasks that relate to authorization, reservations, charging session handling and system tasks like rebooting or firmware updates. + +- Auth orchestrates authorization, utilizing different token providers like RFID reads and token validators. Libocpp mainly acts as a token validator, but in the case of RemoteStartTransactions it acts as a token provider as well +- EvseManager manages the charging session and charging state machine by communicating with a "board support package", a driver for the charging hardware that abstracts away the control pilot, relay control, power meters, etc. The EvseManager also handles reservations. +- System handles firmware updates, log uploads and resets + +The following sections explain the steps you can follow to implement their functionality on your own and integrate the libocpp directly into your charging station software without relying on EVerest. However, in most cases it's much easier to write an EVerest driver using the *everest-core/interfaces/board_support_AC.yaml* interface. + +#### ChargePoint() constructor + +The main entrypoint for libocpp for OCPP1.6 is the ocpp::v16::ChargePoint constructor. +This is defined in `v16/charge_point.hpp` and takes the following parameters: + +- config: a std::string that contains the libocpp 1.6 config. There are example configs that work with a [SteVe](https://github.com/steve-community/steve) installation for example `config/v16/config-docker.json`. + +- share_path: a std::filesystem path containing the path to the OCPP modules folder, for example pointing to */usr/share/everest/modules/OCPP*. This path contains the following files and directories and is installed by the libocpp install target: + + ```bash + . + ├── config-docker.json + ├── config-docker-tls.json + ├── config.json + ├── init.sql + ├── logging.ini + └── profile_schemas + ├── Config.json + ├── Core.json + ├── FirmwareManagement.json + ├── Internal.json + ├── LocalAuthListManagement.json + ├── PnC.json + ├── Reservation.json + ├── Security.json + ├── SmartCharging.json + └── Custom.json + ``` + + Here you can find: + - the aforementioned config files + + - a *logging.ini* that is needed to initialize logging with Everest::Logging::init(path_to_logging_ini, "name_of_binary") + + - a *init.sql* file which contains the database schema used by libocpp for its sqlite database + + - and a *profile_schemas* directory. This contains json schema files that are used to validate the libocpp config. The schemas are split up according to the OCPP1.6 feature profiles like Core, FirmwareManagement and so on. Additionally there is a schema for "Internal" configuration options (for example the ChargePointId, or CentralSystemURI). A "PnC" schema for the ISO 15118 Plug & Charge with OCPP 1.6 Application note, a "Security" schema for the OCPP 1.6 Security Whitepaper (3rd edition) and an exemplary "Custom" schema are provided as well. The Custom.json could be modified to be able to add custom configuration keys. Finally there's a Config.json schema that ties everything together + +- user_config_path: this points to a "user config", which we call a configuration file that's merged with the config that's provided in the "config" parameter. Here you can add, remove and overwrite settings without modifying the config passed in the first parameter directly. This is also used by libocpp to persistently modify config entries that are changed by the CSMS that should persist across restarts. + +- database_path: this points to the location of the sqlite database that libocpp uses to keep track of connector availability, the authorization cache and auth list, charging profiles and transaction data + +- sql_init_path: this points to the aforementioned init.sql file which contains the database schema used by libocpp for its sqlite database + +- message_log_path: this points to the directory in which libocpp can put OCPP communication logfiles for debugging purposes. This behavior can be controlled by the "LogMessages" (set to true by default) and "LogMessagesFormat" (set to ["log", "html", "session_logging"] by default, "console" and "console_detailed" are also available) configuration keys in the "Internal" section of the config file. Please note that this is intended for debugging purposes only as it logs all communication, including authentication messages. + +- evse_security: this is a pointer to an implementation of the `common/evse_security.hpp` interface. This allows you to include your custom implementation of the security related operations according to this interface. If you set this value to nullptr, the internal implementation of the security related operations of libocpp will be used. In this case you need to specify the parameter security_configuration + +- security_configuration: this parameter should only be set in case the evse_security parameter is nullptr. It specifies the file paths that are required to set up the internal evse_security implementation. Note that you need to specify bundle files for the CA certificates and directories for the certificates and keys + + The directory layout expected is as follows + + ```bash + . + ├── ca + │ ├── csms + │ │ └── CSMS_ROOT_CA.pem + │ ├── cso + │ │ ├── CPO_CERT_CHAIN.pem + │ │ ├── CPO_SUB_CA1_LEAF.der + │ │ ├── CPO_SUB_CA1.pem + │ │ ├── CPO_SUB_CA2_LEAF.der + │ │ └── CPO_SUB_CA2.pem + │ ├── mf + │ │ └── MF_ROOT_CA.pem + │ ├── mo + │ │ ├── INTERMEDIATE_MO_CA_CERTS.pem + │ │ ├── MO_ROOT_CA.der + │ │ ├── MO_ROOT_CA.pem + │ │ ├── MO_SUB_CA1.der + │ │ ├── MO_SUB_CA1.pem + │ │ ├── MO_SUB_CA2.der + │ │ └── MO_SUB_CA2.pem + │ └── v2g + │ ├── V2G_ROOT_CA.der + │ └── V2G_ROOT_CA.pem + ├── client + │ ├── csms + │ │ ├── CPO_CERT_CHAIN.pem + │ │ ├── CPO_SUB_CA1.key + │ │ ├── CPO_SUB_CA2.key + │ │ ├── SECC_LEAF.der + │ │ ├── SECC_LEAF.key + │ │ └── SECC_LEAF.pem + │ ├── cso + │ │ ├── CPO_CERT_CHAIN.pem + │ │ ├── CPO_SUB_CA1.key + │ │ ├── CPO_SUB_CA2.key + │ │ ├── SECC_LEAF.der + │ │ ├── SECC_LEAF.key + │ │ └── SECC_LEAF.pem + │ └── v2g + │ └── V2G_ROOT_CA.key + ``` + +#### registering callbacks + +You can (and in many cases MUST) register a number of callbacks so libocpp can interact with the charger. In EVerest most of this functionality is orchestrated by the "EvseManager" module, but you can also register your own callbacks interacting directly with your chargers software. Following is a list of callbacks that you must register and a few words about their purpose. + +TODO: in a future version of libocpp the callbacks will be organised in a struct with optional members emphasizing the required and optional callbacks. + +Some general notes: the "connector" parameter of some of the callbacks refers to the connector number as understood in the OCPP 1.6 specification, "0" means the whole charging station, the connectors with EVSEs used for charging cars start at "1". + +- register_pause_charging_callback + + this callback is used by libocpp to request pausing of charging, the "connector" parameter tells you which connector/EVSE has to pause charging + +- register_resume_charging_callback + + this callback is used by libocpp the request resuming of charging, the "connector" parameter tells you which connector/EVSE can resume charging + +- register_stop_transaction_callback + + in EVerest this calls the EvseManagers stop_transaction command which "Stops transactions and cancels charging externally, charging can only be resumed by replugging car. EVSE will also stop transaction automatically e.g. on disconnect, so this only needs to be called if the transaction should end before." + this will then signal the following events: + - ChargingFinished + - TransactionFinished + +- register_unlock_connector_callback + + can be used by libocpp to force unlock a connector + +- register_reserve_now_callback + + libocpp can use this to reserve a connector, reservation handling is outsourced to a reservation manager in EVerest that implements the reservation interface (everest-core/interfaces/reservation.yaml) + +- register_upload_diagnostics_callback + + uses a function (in EVerest provided by the System module) to upload the requested diagnostics file + +- register_upload_logs_callback + + uses a function (in EVerest provided by the System module) to upload the requested log file + +- register_update_firmware_callback + + uses a function (in EVerest provided by the System module) to perform a firmware update + +- register_signed_update_firmware_callback + + uses a function (in EVerest provided by the System module) to perform a signed firmware update + +- register_provide_token_callback + + this callback is used in a remote start transaction to provide a token (prevalidated or not) to the authorization system + +- register_set_connection_timeout_callback + + used by libocpp to set the authorization or plug in connection timeout in the authorization system based on the "ConnectionTimeout" configuration key + +- register_disable_evse_callback + + used to disable the EVSE (ChangeAvailability.req) + +- register_enable_evse_callback + + used to enable the EVSE (ChangeAvailability.req) + +- register_cancel_reservation_callback + + used to cancel a reservation in the reservation manager (CancelReservation.req) + +- register_signal_set_charging_profiles_callback + + used to signal that new charging schedule(s) have been set, you can then use + get_all_composite_charging_schedules(duration_s) to get the new valid charging schedules + +- register_is_reset_allowed_callback + + used to inquire (in EVerest from the System module) if a reset is allowed + +- register_reset_callback + + used to perform a reset of the requested type + +- register_connection_state_changed_callback + + used to inform about the connection state to the CSMS (connected = true, disconnected = false) + +- register_configuration_key_changed_callback + used to react on a changed configuration key. This callback is called when the specified configuration key has been changed by the CSMS + +#### Functions that need to be triggered from the outside after new information is availble (on_... functions in the charge point API) + +- on_log_status_notification(int32_t request_id, std::string log_status) + + can be used to notify libocpp of a log status notification + +- on_firmware_update_status_notification(int32_t request_id, std::string firmware_update_status) + + can be used to notify libocpp of a firmware update status notification + +- on_meter_values(int32_t connector, const Powermeter& powermeter) + + provides a Powermeter struct to libocpp (for sending meter values during charging sessions or periodically) + +- on_max_current_offered(int32_t connector, int32_t max_current) + + the maximum current offered to the EV on this connector (in ampere) + +#### The following functions are triggered depending on different so called "Session Events" from the EvseManager + +each of these functions will have a small note what the Session Event was and what it triggers in libocpp + +- on_enabled(int32_t connector) + + Notifies libocpp that the connector is functional and operational + +- on_disabled(int32_t connector) + + Notifies libocpp that the connector is disabled + +- on_transaction_started + + Notifies libocpp that a transaction at the given connector has started, this means that authorization is available and the car is plugged in. + + Some of its parameters: + + session_id is an internal session_id originating in the EvseManager to keep track of the transaction, this is NOT to be mistaken for the transactionId from the StartTransactionResponse in OCPP! + + id_token is the token with which the transaction was authenticated + + meter_start contains the meter value in Wh for the connector at start of the transaction + + timestamp at the start of the transaction + +- on_transaction_stopped + + Notifies libocpp that the transaction on the given connector with the given reason has been stopped. + + Some of its parameters: + + timestamp at the end of the transaction + + energy_wh_import contains the meter value in Wh for the connector at end of the transaction + +- on_suspend_charging_ev + + Notifies libocpp that the EV has paused charging + +- on_suspend_charging_evse + + Notifies libocpp that the EVSE has paused charging + +- on_resume_charging + + Notifies libocpp that charging has resumed + +- on_session_started + + this is mostly used for logging and changing the connector state + +- on_session_stopped + + this is mostly used for logging and changing the connector state + +- on_error + + Notify libocpp of an error + +- on_reservation_start + + Notifies libocpp that a reservation has started + +- on_reservation_end + + Notifies libocpp that a reservation has ended + +#### Authorization + +In EVerest authorization is handled by the Auth module and various auth token providers and validators. The OCPP module acts as both a token provider (for pre validated tokens in RemoteStartTransactions) and a token validator (using the authorize requests, or plug & charge) +To use libocpp as a auth token validator (e.g. before starting a transaction) you can call the "authorize_id_token" function of the ChargePoint object. diff --git a/doc/state_chart_1_6.drawio b/doc/v16/state_chart_1_6.drawio similarity index 100% rename from doc/state_chart_1_6.drawio rename to doc/v16/state_chart_1_6.drawio diff --git a/doc/v201/getting_started.md b/doc/v201/getting_started.md new file mode 100644 index 000000000..cd9e65abc --- /dev/null +++ b/doc/v201/getting_started.md @@ -0,0 +1,13 @@ +# Getting Started with OCPP2.0.1 + +## Integrate this library with your Charging Station Implementation for OCPP2.0.1 + +OCPP is a protocol that affects, controls and monitors many areas of a charging station's operation. + +If you want to integrate this library with your charging station implementation, you have to register a couple of **callbacks** and integrate **event handlers**. This is necessary for the library to interact with your charging station according to the requirements of OCPP. + +Libocpp needs registered **callbacks** in order to execute control commands defined within OCPP (e.g ResetRequest or RemoteStartTransactionRequest) + +The implementation must call **event handlers** of libocpp so that the library can track the state of the charging station and trigger OCPP messages accordingly (e.g. MeterValuesRequest , StatusNotificationRequest) + +Your reference within libocpp to interact is a single instance to the class ocpp::v201::ChargePoint defined in `v201/charge_point.hpp` for OCPP 2.0.1. diff --git a/doc/ocpp_201_device_model_initialization.md b/doc/v201/ocpp_201_device_model_initialization.md similarity index 100% rename from doc/ocpp_201_device_model_initialization.md rename to doc/v201/ocpp_201_device_model_initialization.md diff --git a/doc/ocpp_201_monitors.md b/doc/v201/ocpp_201_monitors.md similarity index 97% rename from doc/ocpp_201_monitors.md rename to doc/v201/ocpp_201_monitors.md index c469cb0a5..f76d37b4d 100644 --- a/doc/ocpp_201_monitors.md +++ b/doc/v201/ocpp_201_monitors.md @@ -2,12 +2,12 @@ Monitors are a mechanism for reporting based on certain criteria the internal state of the variables present on the charger. The monitors can be configured in different ways, with custom monitors being sent from the CSMS and HardWired and Preconfigured monitors set up in the config of the database. - ## Basic Configuration The monitors are evaluated from time to time in the case of periodic monitors and after a variable has been modified in the case of monitors that are triggered. Periodic monitors will be handled from time to time, the default being 1 second. -### Variables: +### Variables + - Enabling monitors: set the `MonitoringCtrlrEnabled` variable to true - Periodic monitor process time: set the `MonitorsProcessingInterval` to the desired interval (default 1 second) - To activate monitor processing: set the `ActiveMonitoringBase` variable to `All` @@ -20,7 +20,7 @@ Note: There is a small overhead for the monitoring process interval. The periodi In order to set up pre-existing monitors that are not set up by the CSMS, for the variables that allow monitoring the configuration json file can be extended in the following way: -``` +```json "EVSEPower": { "variable_name": "Power", "characteristics": { @@ -73,4 +73,4 @@ In the example for the 'EVSEPower' variable that supports monitoring there were For more information related to the monitor functionality, please refer to the OCPP201 specification. -Note: for a delta monitor, an initial `reference_value` must be provided or the library will fail to initialize. \ No newline at end of file +Note: for a delta monitor, an initial `reference_value` must be provided or the library will fail to initialize. diff --git a/doc/ocpp_201_monitors_periodic_flowchart.puml b/doc/v201/ocpp_201_monitors_periodic_flowchart.puml similarity index 100% rename from doc/ocpp_201_monitors_periodic_flowchart.puml rename to doc/v201/ocpp_201_monitors_periodic_flowchart.puml diff --git a/doc/ocpp_201_monitors_trigger_flowchart.puml b/doc/v201/ocpp_201_monitors_trigger_flowchart.puml similarity index 100% rename from doc/ocpp_201_monitors_trigger_flowchart.puml rename to doc/v201/ocpp_201_monitors_trigger_flowchart.puml diff --git a/doc/ocpp_201_status.md b/doc/v201/ocpp_201_status.md similarity index 99% rename from doc/ocpp_201_status.md rename to doc/v201/ocpp_201_status.md index 43e4afb7e..1f60bf410 100644 --- a/doc/ocpp_201_status.md +++ b/doc/v201/ocpp_201_status.md @@ -1,3 +1,5 @@ +# OCPP2.0.1 Functional Requirements Status + This document contains the status of which OCPP 2.0.1 numbered functional requirements (FRs) have been implemented in `libocpp`. This does not cover if the functionality is also implemented in `everest-core`. ## Legend @@ -812,16 +814,11 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | E09.FR.06 | ✅ | | | E09.FR.07 | ✅ | | -## Transactions - When cable disconnected on EV-side: Suspend Transaction +## Transactions - When cable disconnected on EV-side: Suspend Transaction | ID | Status | Remark | |-----------|--------|--------| | E10.FR.01 | | | - -## Transactions - When cable disconnected on EV-side: Stop Transaction - -| ID | Status | Remark | -|-----------|--------|--------| | E10.FR.02 | ✅ | | | E10.FR.03 | ✅ | | | E10.FR.04 | ✅ | | diff --git a/doc/resumeInterruptedTransactions/resumeInterruptedTransactions.png b/doc/v201/resumeInterruptedTransactions/resumeInterruptedTransactions.png similarity index 100% rename from doc/resumeInterruptedTransactions/resumeInterruptedTransactions.png rename to doc/v201/resumeInterruptedTransactions/resumeInterruptedTransactions.png diff --git a/doc/resumeInterruptedTransactions/resumeInterruptedTransactions.puml b/doc/v201/resumeInterruptedTransactions/resumeInterruptedTransactions.puml similarity index 100% rename from doc/resumeInterruptedTransactions/resumeInterruptedTransactions.puml rename to doc/v201/resumeInterruptedTransactions/resumeInterruptedTransactions.puml diff --git a/include/ocpp/common/constants.hpp b/include/ocpp/common/constants.hpp index 6fa16910e..cf96edf9a 100644 --- a/include/ocpp/common/constants.hpp +++ b/include/ocpp/common/constants.hpp @@ -3,6 +3,7 @@ #pragma once +#include #include namespace ocpp { @@ -22,4 +23,6 @@ constexpr float LOW_VOLTAGE = 230; constexpr std::int32_t NO_START_PERIOD = -1; constexpr std::int32_t EVSEID_NOT_SET = -1; -} // namespace ocpp \ No newline at end of file +constexpr std::chrono::seconds DEFAULT_WAIT_FOR_FUTURE_TIMEOUT = std::chrono::seconds(60); + +} // namespace ocpp diff --git a/include/ocpp/common/message_dispatcher.hpp b/include/ocpp/common/message_dispatcher.hpp new file mode 100644 index 000000000..20594d507 --- /dev/null +++ b/include/ocpp/common/message_dispatcher.hpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +namespace ocpp { + +/// \brief Interface for dispatching OCPP messages that shall be send over the websocket. This interface defines +/// dispatching of Call, CallResult and CallError messages. +/// \tparam T Type specifies the OCPP protocol version +template class MessageDispatcherInterface { + +public: + virtual ~MessageDispatcherInterface(){}; + + /// \brief Dispatches a Call message. + /// \param call the OCPP Call message. + /// \param triggered indicates if the call was triggered by a TriggerMessage. Default is false. + virtual void dispatch_call(const json& call, bool triggered = false) = 0; + + /// \brief Dispatches a Call message asynchronously. + /// \param call the OCPP Call message. + /// \param triggered indicates if the call was triggered by a TriggerMessage. Default is false. + /// \return std::future> Future object containing the enhanced message + /// result of type T. + virtual std::future> dispatch_call_async(const json& call, bool triggered = false) = 0; + + /// \brief Dispatches a CallResult message. + /// \param call_result the OCPP CallResult message. + virtual void dispatch_call_result(const json& call_result) = 0; + + /// \brief Dispatches a CallError message. + /// \param call_result the OCPP CallError message. + virtual void dispatch_call_error(const json& call_error) = 0; +}; + +} // namespace ocpp diff --git a/include/ocpp/common/message_queue.hpp b/include/ocpp/common/message_queue.hpp index b7df66cf4..63dc0e59c 100644 --- a/include/ocpp/common/message_queue.hpp +++ b/include/ocpp/common/message_queue.hpp @@ -561,6 +561,9 @@ template class MessageQueue { case QueueType::Transaction: this->transaction_message_queue.erase(selected_transaction_message_it); break; + case QueueType::None: + // do nothing + break; default: break; @@ -627,16 +630,7 @@ template class MessageQueue { } } - /// \brief pushes a new \p call message onto the message queue - template void push(Call call, const bool stall_until_accepted = false) { - if (!running) { - return; - } - json call_json = call; - push(call_json, stall_until_accepted); - } - - void push(const json& message, const bool stall_until_accepted = false) { + void push_call(const json& message, const bool stall_until_accepted = false) { if (!running) { return; } @@ -661,16 +655,15 @@ template class MessageQueue { } /// \brief Sends a new \p call_result message over the websocket - template void push(CallResult call_result) { + void push_call_result(const json& call_result) { if (!running) { return; } - this->send_callback(call_result); { std::lock_guard lk(this->next_message_mutex); if (next_message_to_send.has_value()) { - if (next_message_to_send.value() == call_result.uniqueId) { + if (next_message_to_send.value() == call_result.at(MESSAGE_ID)) { next_message_to_send.reset(); } } @@ -680,7 +673,7 @@ template class MessageQueue { } /// \brief Sends a new \p call_error message over the websocket - void push(CallError call_error) { + void push_call_error(CallError call_error) { if (!running) { return; } @@ -700,7 +693,7 @@ template class MessageQueue { /// \brief pushes a new \p call message onto the message queue /// \returns a future from which the CallResult can be extracted - template std::future> push_async(Call call) { + std::future> push_call_async(const json& call) { auto message = std::make_shared>(call); if (!running) { diff --git a/include/ocpp/common/schemas.hpp b/include/ocpp/common/schemas.hpp index becbf7dac..c332ab5fc 100644 --- a/include/ocpp/common/schemas.hpp +++ b/include/ocpp/common/schemas.hpp @@ -38,6 +38,10 @@ class Schemas { public: /// \brief Creates a new Schemas object looking for the root schema file in relation to the provided \p main_dir explicit Schemas(fs::path schemas_path); + /// \brief Creates a new Schemas object using the supplied JSON schema + explicit Schemas(const json& schema_in); + /// \brief Creates a new Schemas object using the supplied JSON schema + explicit Schemas(json&& schema_in); /// \brief Provides the config schema /// \returns the config schema as as json object diff --git a/include/ocpp/v16/charge_point.hpp b/include/ocpp/v16/charge_point.hpp index fd3702947..67746d726 100644 --- a/include/ocpp/v16/charge_point.hpp +++ b/include/ocpp/v16/charge_point.hpp @@ -41,6 +41,13 @@ class ChargePoint { private: std::unique_ptr charge_point; + /// \addtogroup chargepoint_constructors Chargepoint constructors + /// Constructors for chargepoint, 1.6 and 2.0.1 + /// @{ + + /// @name Constructors for 1.6 + /// @{ + public: /// \brief The main entrypoint for libOCPP for OCPP 1.6 /// \param config a nlohmann json config object that contains the libocpp 1.6 config. There are example configs that @@ -71,6 +78,9 @@ class ChargePoint { ~ChargePoint(); + /// @} // End constructors 1.6 group + /// @} // End chargepoint constructors topic + /// \brief Starts the ChargePoint, initializes and connects to the Websocket endpoint and initializes a /// BootNotification.req /// \param connector_status_map initial state of connectors including connector 0 with reduced set of states @@ -176,6 +186,14 @@ class ChargePoint { get_all_enhanced_composite_charging_schedules(const int32_t duration_s, const ChargingRateUnit unit = ChargingRateUnit::A); + /// \addtogroup ocpp16_handlers OCPP 1.6 handlers + /// Handlers that can be called from the implementing class. + /// @{ + + /// @name Handlers + /// The handlers + /// @{ + /// \brief Stores the given \p powermeter values for the given \p connector . This function can be called when a new /// meter value is present. /// \param connector @@ -334,6 +352,18 @@ class ChargePoint { /// \param request ChangeAvailabilityResponse on_change_availability(const ChangeAvailabilityRequest& request); + /// @} // End handlers group + + /// @} + + /// @addtogroup ocpp16_callbacks OCPP 1.6 callbacks + /// Callbacks will call be called when necessary and must be implemented by the calling class. + /// @{ + + /// @name Callbacks + /// Callbacks + /// @{ + /// registers a \p callback function that can be used to receive a arbitrary data transfer for the given \p /// vendorId and \p messageId /// \param vendorId @@ -527,35 +557,42 @@ class ChargePoint { void register_security_event_callback( const std::function& callback); - /// \brief Gets the configured configuration key requested in the given \p request - /// \param request specifies the keys that should be returned. If empty or not set, all keys will be reported - /// \return a response containing the requested key(s) including the values and unkown keys if present - GetConfigurationResponse get_configuration_key(const GetConfigurationRequest& request); - - /// \brief Sets a custom configuration key - /// \param key - /// \param value - /// \return Indicates the result of the operation - ConfigurationStatus set_custom_configuration_key(CiString<50> key, CiString<500> value); - /// \brief registers a \p callback function that can be used to check, if the \p connector is reserved for the given /// \p id_token. The is_token_reserved_for_connector_callback is called when a RemoteStartTransaction.req is /// received. /// \param callback + /// \ingroup ocpp16_callbacks void register_is_token_reserved_for_connector_callback( const std::function& callback); /// \brief Registers a callback function for the session cost datatransfer message (California Pricing Requirements) /// \param session_cost_callback The callback. + /// \ingroup ocpp16_callbacks void register_session_cost_callback( const std::function& session_cost_callback); /// \brief Register a callback function for display messages (used in California Pricing Requirements) /// \param set_display_message_callback The callback. + /// \ingroup ocpp16_callbacks void register_set_display_message_callback( const std::function&)> set_display_message_callback); + /// @} // End ocpp 16 callbacks group / topic + + /// @} // End group + + /// \brief Gets the configured configuration key requested in the given \p request + /// \param request specifies the keys that should be returned. If empty or not set, all keys will be reported + /// \return a response containing the requested key(s) including the values and unkown keys if present + GetConfigurationResponse get_configuration_key(const GetConfigurationRequest& request); + + /// \brief Sets a custom configuration key + /// \param key + /// \param value + /// \return Indicates the result of the operation + ConfigurationStatus set_custom_configuration_key(CiString<50> key, CiString<500> value); + /// \brief Delay draining the message queue after reconnecting, so the CSMS can perform post-reconnect checks first /// \param delay The delay period (seconds) void set_message_queue_resume_delay(std::chrono::seconds delay); diff --git a/include/ocpp/v16/charge_point_configuration.hpp b/include/ocpp/v16/charge_point_configuration.hpp index 7e9171a3b..948c2d08f 100644 --- a/include/ocpp/v16/charge_point_configuration.hpp +++ b/include/ocpp/v16/charge_point_configuration.hpp @@ -84,6 +84,9 @@ class ChargePointConfiguration { KeyValue getLogRotationMaximumFileCountKeyValue(); std::vector getSupportedChargingProfilePurposeTypes(); KeyValue getSupportedChargingProfilePurposeTypesKeyValue(); + std::vector getIgnoredProfilePurposesOffline(); + std::optional getIgnoredProfilePurposesOfflineKeyValue(); + bool setIgnoredProfilePurposesOffline(const std::string& ignored_profile_purposes_offline); int32_t getMaxCompositeScheduleDuration(); KeyValue getMaxCompositeScheduleDurationKeyValue(); std::optional getCompositeScheduleDefaultLimitAmps(); diff --git a/include/ocpp/v16/charge_point_impl.hpp b/include/ocpp/v16/charge_point_impl.hpp index bcb31bdd2..0f79a59d9 100644 --- a/include/ocpp/v16/charge_point_impl.hpp +++ b/include/ocpp/v16/charge_point_impl.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -87,7 +89,7 @@ class ChargePointImpl : ocpp::ChargingStationBase { BootReasonEnum bootreason; ChargePointConnectionState connection_state; bool boot_notification_callerror; - RegistrationStatus registration_status; + std::atomic registration_status; DiagnosticsStatus diagnostics_status; FirmwareStatus firmware_status; bool firmware_update_is_pending = false; @@ -95,6 +97,7 @@ class ChargePointImpl : ocpp::ChargingStationBase { std::string message_log_path; std::unique_ptr websocket; + std::unique_ptr> message_dispatcher; Everest::SteadyTimer websocket_timer; std::unique_ptr> message_queue; std::map> connectors; @@ -205,11 +208,6 @@ class ChargePointImpl : ocpp::ChargingStationBase { std::unique_ptr> create_message_queue(); void message_callback(const std::string& message); void handle_message(const EnhancedMessage& message); - template bool send(Call call, bool initiated_by_trigger_message = false); - template - std::future> send_async(Call call, bool initiated_by_trigger_message = false); - template bool send(CallResult call_result); - bool send(CallError call_error); void heartbeat(bool initiated_by_trigger_message = false); void boot_notification(bool initiated_by_trigger_message = false); void clock_aligned_meter_values_sample(); diff --git a/include/ocpp/v16/message_dispatcher.hpp b/include/ocpp/v16/message_dispatcher.hpp new file mode 100644 index 000000000..774ebf8db --- /dev/null +++ b/include/ocpp/v16/message_dispatcher.hpp @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include + +namespace ocpp { +namespace v16 { + +class MessageDispatcher : public MessageDispatcherInterface { + +public: + MessageDispatcher(ocpp::MessageQueue& message_queue, ChargePointConfiguration& configuration, + std::atomic& registration_status) : + message_queue(message_queue), configuration(configuration), registration_status(registration_status){}; + void dispatch_call(const json& call, bool triggered = false) override; + std::future> dispatch_call_async(const json& call, bool triggered) override; + void dispatch_call_result(const json& call_result) override; + void dispatch_call_error(const json& call_error) override; + +private: + ocpp::MessageQueue& message_queue; + ChargePointConfiguration& configuration; + std::atomic& registration_status; +}; + +} // namespace v16 +} // namespace ocpp diff --git a/include/ocpp/v16/smart_charging.hpp b/include/ocpp/v16/smart_charging.hpp index 251999df6..44a5fda31 100644 --- a/include/ocpp/v16/smart_charging.hpp +++ b/include/ocpp/v16/smart_charging.hpp @@ -98,11 +98,12 @@ class SmartChargingHandler { void clear_all_profiles(); /// - /// \brief Gets all valid profiles within the given absoulte \p start_time and absolute \p end_time for the given \p - /// connector_id + /// \brief Gets all valid profiles within the given absoulte \p start_time and absolute \p end_time for the given + /// \p connector_id . Only profiles that are not contained in \p purposes_to_ignore are included in the response. /// - std::vector get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, - const int connector_id); + std::vector + get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int connector_id, + const std::set& purposes_to_ignore = {}); /// /// \brief Calculates the enhanced composite schedule for the given \p valid_profiles and the given \p connector_id /// diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index ef3006aa0..02e5023b2 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -7,6 +7,8 @@ #include #include +#include + #include #include @@ -14,7 +16,7 @@ #include #include #include -#include +#include #include #include #include @@ -108,6 +110,14 @@ class ChargePointInterface { /// \brief Disconnects the the websocket connection to the CSMS if it is connected virtual void disconnect_websocket() = 0; + /// \addtogroup ocpp201_handlers OCPP 2.0.1 handlers + /// Handlers that can be called from the implementing class + /// @{ + + /// @name Handlers + /// The handlers + /// @{ + /// /// \brief Can be called when a network is disconnected, for example when an ethernet cable is removed. /// @@ -231,19 +241,6 @@ class ChargePointInterface { on_charging_state_changed(const uint32_t evse_id, const ChargingStateEnum charging_state, const TriggerReasonEnum trigger_reason = TriggerReasonEnum::ChargingStateChanged) = 0; - /// \brief Gets the transaction id for a certain \p evse_id if there is an active transaction - /// \param evse_id The evse to tet the transaction for - /// \return The transaction id if a transaction is active, otherwise nullopt - virtual std::optional get_evse_transaction_id(int32_t evse_id) = 0; - - /// \brief Validates provided \p id_token \p certificate and \p ocsp_request_data using CSMS, AuthCache or AuthList - /// \param id_token - /// \param certificate - /// \param ocsp_request_data - /// \return AuthorizeResponse containing the result of the validation - virtual AuthorizeResponse validate_token(const IdToken id_token, const std::optional>& certificate, - const std::optional>& ocsp_request_data) = 0; - /// \brief Event handler that can be called to trigger a NotifyEvent.req with the given \p events /// \param events virtual void on_event(const std::vector& events) = 0; @@ -276,6 +273,23 @@ class ChargePointInterface { /// virtual void on_variable_changed(const SetVariableData& set_variable_data) = 0; + /// @} // End handlers group + + /// @} + + /// \brief Gets the transaction id for a certain \p evse_id if there is an active transaction + /// \param evse_id The evse to tet the transaction for + /// \return The transaction id if a transaction is active, otherwise nullopt + virtual std::optional get_evse_transaction_id(int32_t evse_id) = 0; + + /// \brief Validates provided \p id_token \p certificate and \p ocsp_request_data using CSMS, AuthCache or AuthList + /// \param id_token + /// \param certificate + /// \param ocsp_request_data + /// \return AuthorizeResponse containing the result of the validation + virtual AuthorizeResponse validate_token(const IdToken id_token, const std::optional>& certificate, + const std::optional>& ocsp_request_data) = 0; + /// \brief Data transfer mechanism initiated by charger /// \param vendorId /// \param messageId @@ -373,6 +387,8 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::unique_ptr evse_manager; std::unique_ptr connectivity_manager; + std::unique_ptr> message_dispatcher; + // utility std::shared_ptr> message_queue; std::shared_ptr database_handler; @@ -401,7 +417,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::atomic_bool stop_auth_cache_cleanup_handler; // states - RegistrationStatusEnum registration_status; + std::atomic registration_status; FirmwareStatusEnum firmware_status; // The request ID in the last firmware update status received std::optional firmware_status_id; @@ -453,8 +469,6 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// \brief optional delay to resumption of message queue after reconnecting to the CSMS std::chrono::seconds message_queue_resume_delay = std::chrono::seconds(0); - bool send(CallError call_error); - // internal helper functions void initialize(const std::map& evse_connector_structure, const std::string& message_log_path); void init_certificate_expiration_check_timers(); @@ -471,7 +485,9 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void trigger_authorization_cache_cleanup(); void cache_cleanup_handler(); - GetCompositeScheduleResponse get_composite_schedule_internal(const GetCompositeScheduleRequest& request); + GetCompositeScheduleResponse + get_composite_schedule_internal(const GetCompositeScheduleRequest& request, + const std::set& profiles_to_ignore = {}); /// \brief Removes all network connection profiles below the actual security profile and stores the new list in the /// device model @@ -745,20 +761,13 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa // Functional Block P: DataTransfer void handle_data_transfer_req(Call call); - // general message handling - template bool send(ocpp::Call call, const bool initiated_by_trigger_message = false); - - template std::future> send_async(ocpp::Call call); - - template bool send(ocpp::CallResult call_result); - // Generates async sending callbacks template std::function send_callback(MessageType expected_response_message_type) { return [this, expected_response_message_type](auto request) { MessageId message_id = MessageId(to_string(this->uuid_generator())); const auto enhanced_response = - this->send_async(ocpp::Call(request, message_id)).get(); + this->message_dispatcher->dispatch_call_async(ocpp::Call(request, message_id)).get(); if (enhanced_response.messageType != expected_response_message_type) { throw UnexpectedMessageTypeFromCSMS( std::string("Got unexpected message type from CSMS, expected: ") + @@ -786,6 +795,12 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void clear_invalid_charging_profiles(); public: + /// \addtogroup chargepoint_constructors + /// @{ + + /// @name Constructors for 2.0.1 + /// @{ + /// \brief Construct a new ChargePoint object /// \param evse_connector_structure Map that defines the structure of EVSE and connectors of the chargepoint. The /// key represents the id of the EVSE and the value represents the number of connectors for this EVSE. The ids of @@ -805,17 +820,17 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// \param evse_connector_structure Map that defines the structure of EVSE and connectors of the chargepoint. The /// key represents the id of the EVSE and the value represents the number of connectors for this EVSE. The ids of /// the EVSEs have to increment starting with 1. - /// \param device_model_storage device model storage instance + /// \param device_model_storage_interface device model interface instance /// \param ocpp_main_path Path where utility files for OCPP are read and written to /// \param core_database_path Path to directory where core database is located /// \param message_log_path Path to where logfiles are written to /// \param evse_security Pointer to evse_security that manages security related operations /// \param callbacks Callbacks that will be registered for ChargePoint ChargePoint(const std::map& evse_connector_structure, - std::unique_ptr device_model_storage, const std::string& ocpp_main_path, - const std::string& core_database_path, const std::string& sql_init_path, - const std::string& message_log_path, const std::shared_ptr evse_security, - const Callbacks& callbacks); + std::unique_ptr device_model_storage_interface, + const std::string& ocpp_main_path, const std::string& core_database_path, + const std::string& sql_init_path, const std::string& message_log_path, + const std::shared_ptr evse_security, const Callbacks& callbacks); /// \brief Construct a new ChargePoint object /// \param evse_connector_structure Map that defines the structure of EVSE and connectors of the chargepoint. The @@ -838,6 +853,10 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa const std::string& sql_init_path, const std::string& message_log_path, const std::shared_ptr evse_security, const Callbacks& callbacks); + /// @} // End chargepoint 2.0.1 member group + + /// @} // End chargepoint 2.0.1 topic + ~ChargePoint(); void start(BootReasonEnum bootreason = BootReasonEnum::PowerUp) override; diff --git a/include/ocpp/v201/charge_point_callbacks.hpp b/include/ocpp/v201/charge_point_callbacks.hpp index 521556159..5804f25a7 100644 --- a/include/ocpp/v201/charge_point_callbacks.hpp +++ b/include/ocpp/v201/charge_point_callbacks.hpp @@ -19,6 +19,14 @@ namespace ocpp::v201 { struct Callbacks { + /// @addtogroup ocpp201_callbacks OCPP 2.0.1 callbacks + /// Callbacks will call be called when necessary and must be implemented by the calling class. + /// @{ + + /// @name Callbacks + /// Callbacks + /// @{ + /// \brief Function to check if the callback struct is completely filled. All std::functions should hold a function, /// all std::optional should either be empty or hold a function. /// \param device_model The device model, to check if certain modules are enabled / available. @@ -151,5 +159,9 @@ struct Callbacks { std::optional currency_code)>> set_running_cost_callback; + + /// @} // End ocpp 201 callbacks group / topic + + /// @} // End group }; } // namespace ocpp::v201 diff --git a/include/ocpp/v201/connectivity_manager.hpp b/include/ocpp/v201/connectivity_manager.hpp index e72360209..03c45bc6f 100644 --- a/include/ocpp/v201/connectivity_manager.hpp +++ b/include/ocpp/v201/connectivity_manager.hpp @@ -202,7 +202,8 @@ class ConnectivityManager { void next_network_configuration_priority(); /// @brief Cache all the network connection profiles. Must be called once during initialization - void cache_network_connection_profiles(); + /// \return True if the network connection profiles could be cached, else False. + bool cache_network_connection_profiles(); }; } // namespace v201 diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 8099f5aa7..93af79ac1 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -216,6 +216,7 @@ extern const RequiredComponentVariable& SupplyVoltage; extern const ComponentVariable& Phases3to1; extern const RequiredComponentVariable& ChargingProfileMaxStackLevel; extern const RequiredComponentVariable& ChargingScheduleChargingRateUnit; +extern const ComponentVariable& IgnoredProfilePurposesOffline; extern const ComponentVariable& TariffCostCtrlrAvailableTariff; extern const ComponentVariable& TariffCostCtrlrAvailableCost; extern const RequiredComponentVariable& TariffCostCtrlrCurrency; diff --git a/include/ocpp/v201/device_model.hpp b/include/ocpp/v201/device_model.hpp index 51b8f5c5c..72418ab19 100644 --- a/include/ocpp/v201/device_model.hpp +++ b/include/ocpp/v201/device_model.hpp @@ -8,7 +8,7 @@ #include -#include +#include namespace ocpp { namespace v201 { @@ -91,13 +91,13 @@ typedef std::function on_monitor_updated; -/// \brief This class manages access to the device model representation and to the device model storage and provides +/// \brief This class manages access to the device model representation and to the device model interface and provides /// functionality to support the use cases defined in the functional block Provisioning class DeviceModel { private: - DeviceModelMap device_model; - std::unique_ptr storage; + DeviceModelMap device_model_map; + std::unique_ptr device_model; /// \brief Listener for the internal change of a variable on_variable_changed variable_listener; @@ -106,7 +106,7 @@ class DeviceModel { /// \brief Private helper method that does some checks with the device model representation in memory to evaluate if /// a value for the given parameters can be requested. If it can be requested it will be retrieved from the device - /// model storage and the given \p value will be set to the value that was retrieved + /// model interface and the given \p value will be set to the value that was retrieved /// \param component_id /// \param variable_id /// \param attribute_enum @@ -138,15 +138,15 @@ class DeviceModel { public: /// \brief Constructor for the device model - /// \param device_model_storage pointer to a device model storage class - explicit DeviceModel(std::unique_ptr device_model_storage); + /// \param device_model_storage_interface pointer to a device model interface class + explicit DeviceModel(std::unique_ptr device_model_storage_interface); /// \brief Direct access to value of a VariableAttribute for the given component, variable and attribute_enum. This /// should only be called for variables that have a role standardized in the OCPP2.0.1 specification. /// \tparam T datatype of the value that is requested /// \param component_variable Combination of Component and Variable that identifies the Variable /// \param attribute_enum defaults to AttributeEnum::Actual - /// \return the requested value from the device model storage + /// \return the requested value from the device model interface template T get_value(const RequiredComponentVariable& component_variable, const AttributeEnum& attribute_enum = AttributeEnum::Actual) const { @@ -159,11 +159,10 @@ class DeviceModel { if (response == GetVariableStatusEnum::Accepted) { return to_specific_type(value); } else { - EVLOG_critical - << "Directly requested value for ComponentVariable that doesn't exist in the device model storage: " - << component_variable; + EVLOG_critical << "Directly requested value for ComponentVariable that doesn't exist in the device model: " + << component_variable; EVLOG_AND_THROW(std::runtime_error( - "Directly requested value for ComponentVariable that doesn't exist in the device model storage.")); + "Directly requested value for ComponentVariable that doesn't exist in the device model.")); } } @@ -190,7 +189,7 @@ class DeviceModel { } /// \brief Requests a value of a VariableAttribute specified by combination of \p component_id and \p variable_id - /// from the device model storage + /// from the device model /// \tparam T datatype of the value that is requested /// \param component_id /// \param variable_id diff --git a/include/ocpp/v201/device_model_storage.hpp b/include/ocpp/v201/device_model_storage_interface.hpp similarity index 91% rename from include/ocpp/v201/device_model_storage.hpp rename to include/ocpp/v201/device_model_storage_interface.hpp index 2892a781e..b666874e4 100644 --- a/include/ocpp/v201/device_model_storage.hpp +++ b/include/ocpp/v201/device_model_storage_interface.hpp @@ -1,8 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest -#ifndef OCPP_V201_DEVICE_MODEL_STORAGE_HPP -#define OCPP_V201_DEVICE_MODEL_STORAGE_HPP +#pragma once #include #include @@ -34,20 +33,21 @@ struct VariableMonitoringPeriodic { struct VariableMetaData { VariableCharacteristics characteristics; std::unordered_map monitors; + std::optional source; }; using VariableMap = std::map; using DeviceModelMap = std::map; -class DeviceModelStorageError : public std::exception { +class DeviceModelError : public std::exception { public: [[nodiscard]] const char* what() const noexcept override { return this->reason.c_str(); } - explicit DeviceModelStorageError(std::string msg) { + explicit DeviceModelError(std::string msg) { this->reason = std::move(msg); } - explicit DeviceModelStorageError(const char* msg) { + explicit DeviceModelError(const char* msg) { this->reason = std::string(msg); } @@ -55,15 +55,15 @@ class DeviceModelStorageError : public std::exception { std::string reason; }; -/// \brief Abstract base class for device model storage. This class provides an interface for accessing and modifying +/// \brief Abstract base class for device model interface. This class provides an interface for accessing and modifying /// device model data. Implementations of this class should provide concrete implementations for the virtual methods /// declared here. -class DeviceModelStorage { +class DeviceModelStorageInterface { public: - virtual ~DeviceModelStorage() = default; + virtual ~DeviceModelStorageInterface() = default; - /// \brief Gets the device model from the device model storage + /// \brief Gets the device model from the device model interface /// \return std::map> that will contain a full representation of the /// device model except for the VariableAttribute(s) of each Variable. virtual DeviceModelMap get_device_model() = 0; @@ -140,5 +140,3 @@ class DeviceModelStorage { } // namespace v201 } // namespace ocpp - -#endif // OCPP_V201_DEVICE_MODEL_STORAGE_HPP diff --git a/include/ocpp/v201/device_model_storage_sqlite.hpp b/include/ocpp/v201/device_model_storage_sqlite.hpp index 662e4ef01..e5370f59c 100644 --- a/include/ocpp/v201/device_model_storage_sqlite.hpp +++ b/include/ocpp/v201/device_model_storage_sqlite.hpp @@ -9,12 +9,12 @@ #include #include -#include +#include namespace ocpp { namespace v201 { -class DeviceModelStorageSqlite : public DeviceModelStorage { +class DeviceModelStorageSqlite : public DeviceModelStorageInterface { private: std::unique_ptr db; diff --git a/include/ocpp/v201/enums.hpp b/include/ocpp/v201/enums.hpp index 88ad67724..cb13ab4d3 100644 --- a/include/ocpp/v201/enums.hpp +++ b/include/ocpp/v201/enums.hpp @@ -5,6 +5,8 @@ #ifndef OCPP_V201_ENUMS_HPP #define OCPP_V201_ENUMS_HPP +#include + namespace ocpp { namespace v201 { diff --git a/include/ocpp/v201/init_device_model_db.hpp b/include/ocpp/v201/init_device_model_db.hpp index 709b29c9c..a74cca055 100644 --- a/include/ocpp/v201/init_device_model_db.hpp +++ b/include/ocpp/v201/init_device_model_db.hpp @@ -36,7 +36,7 @@ #include #include -#include +#include namespace ocpp::v201 { /// @@ -93,6 +93,8 @@ struct DeviceModelVariable { std::optional default_actual_value; /// \brief Config monitors, if any std::vector monitors; + /// \brief Source of the variable. + std::optional source; }; /// \brief Convert from json to a ComponentKey struct. diff --git a/include/ocpp/v201/message_dispatcher.hpp b/include/ocpp/v201/message_dispatcher.hpp new file mode 100644 index 000000000..caebb462c --- /dev/null +++ b/include/ocpp/v201/message_dispatcher.hpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include + +namespace ocpp { +namespace v201 { + +class MessageDispatcher : public MessageDispatcherInterface { + +public: + MessageDispatcher(ocpp::MessageQueue& message_queue, DeviceModel& device_model, + std::atomic& registration_status) : + message_queue(message_queue), device_model(device_model), registration_status(registration_status){}; + void dispatch_call(const json& call, bool triggered = false) override; + std::future> dispatch_call_async(const json& call, bool triggered) override; + void dispatch_call_result(const json& call_result) override; + void dispatch_call_error(const json& call_error) override; + +private: + ocpp::MessageQueue& message_queue; + DeviceModel& device_model; + std::atomic& registration_status; +}; + +} // namespace v201 +} // namespace ocpp diff --git a/include/ocpp/v201/monitoring_updater.hpp b/include/ocpp/v201/monitoring_updater.hpp index 6337ea939..69486ff76 100644 --- a/include/ocpp/v201/monitoring_updater.hpp +++ b/include/ocpp/v201/monitoring_updater.hpp @@ -11,7 +11,7 @@ #include #include -#include +#include namespace ocpp::v201 { @@ -187,4 +187,4 @@ class MonitoringUpdater { std::unordered_map updater_monitors_meta; }; -} // namespace ocpp::v201 \ No newline at end of file +} // namespace ocpp::v201 diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 09934f370..0af62d011 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -91,7 +91,8 @@ class SmartChargingHandlerInterface { virtual std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const = 0; - virtual std::vector get_valid_profiles(int32_t evse_id) = 0; + virtual std::vector + get_valid_profiles(int32_t evse_id, const std::set& purposes_to_ignore = {}) = 0; virtual CompositeSchedule calculate_composite_schedule(std::vector& valid_profiles, const ocpp::DateTime& start_time, @@ -153,9 +154,12 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const override; - /// \brief Retrieves all profiles that should be considered for calculating the composite schedule. + /// \brief Retrieves all profiles that should be considered for calculating the composite schedule. Only profiles + /// that belong to the given \p evse_id and that are not contained in \p purposes_to_ignore are included in the + /// response. /// - std::vector get_valid_profiles(int32_t evse_id) override; + std::vector + get_valid_profiles(int32_t evse_id, const std::set& purposes_to_ignore = {}) override; /// /// \brief Calculates the composite schedule for the given \p valid_profiles and the given \p connector_id @@ -215,7 +219,8 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { private: std::vector get_evse_specific_tx_default_profiles() const; std::vector get_station_wide_tx_default_profiles() const; - std::vector get_valid_profiles_for_evse(int32_t evse_id); + std::vector + get_valid_profiles_for_evse(int32_t evse_id, const std::set& purposes_to_ignore = {}); void conform_schedule_number_phases(int32_t profile_id, ChargingSchedulePeriod& charging_schedule_period) const; void conform_validity_periods(ChargingProfile& profile) const; CurrentPhaseType get_current_phase_type(const std::optional evse_opt) const; diff --git a/include/ocpp/v201/utils.hpp b/include/ocpp/v201/utils.hpp index ef23aa24a..4a68a9cb5 100644 --- a/include/ocpp/v201/utils.hpp +++ b/include/ocpp/v201/utils.hpp @@ -3,6 +3,8 @@ #ifndef V201_UTILS_HPP #define V201_UTILS_HPP +#include + #include #include namespace ocpp { @@ -71,6 +73,9 @@ std::optional get_total_power_active_import(const MeterValue& meter_value /// \brief Determines if a given \p security_event is critical as defined in the OCPP 2.0.1 appendix bool is_critical(const std::string& security_event); +/// \brief Converts the given \p csl of ChargingProfilePurpose strings into a std::set +std::set get_purposes_to_ignore(const std::string& csl, const bool is_offline); + } // namespace utils } // namespace v201 } // namespace ocpp diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 3b4cad2c3..1513d1033 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -38,6 +38,7 @@ if(LIBOCPP_ENABLE_V16) ocpp/v16/charge_point.cpp ocpp/v16/database_handler.cpp ocpp/v16/charge_point_impl.cpp + ocpp/v16/message_dispatcher.cpp ocpp/v16/smart_charging.cpp ocpp/v16/charge_point_configuration.cpp ocpp/v16/charge_point_state_machine.cpp @@ -80,6 +81,7 @@ if(LIBOCPP_ENABLE_V201) ocpp/v201/utils.cpp ocpp/v201/component_state_manager.cpp ocpp/v201/connectivity_manager.cpp + ocpp/v201/message_dispatcher.cpp ) add_subdirectory(ocpp/v201/messages) add_subdirectory(ocpp/v21/messages) diff --git a/lib/ocpp/common/ocpp_logging.cpp b/lib/ocpp/common/ocpp_logging.cpp index e9e9e5ae6..86c5816ed 100644 --- a/lib/ocpp/common/ocpp_logging.cpp +++ b/lib/ocpp/common/ocpp_logging.cpp @@ -193,7 +193,7 @@ LogRotationStatus MessageLogging::rotate_log(const std::string& file_basename) { std::filesystem::rename(file, new_file_name); EVLOG_info << "Renaming: " << file.string() << " -> " << new_file_name.string(); if (status == LogRotationStatus::NotRotated) { - status == LogRotationStatus::Rotated; + status = LogRotationStatus::Rotated; } } else { // try parsing the .x extension and log an error if this was not possible @@ -210,7 +210,7 @@ LogRotationStatus MessageLogging::rotate_log(const std::string& file_basename) { EVLOG_info << "Renaming: " << file.string() << " -> " << new_file_name.string(); std::filesystem::rename(file, new_file_name); if (status == LogRotationStatus::NotRotated) { - status == LogRotationStatus::Rotated; + status = LogRotationStatus::Rotated; } } diff --git a/lib/ocpp/common/schemas.cpp b/lib/ocpp/common/schemas.cpp index 95b62f96b..5df2df417 100644 --- a/lib/ocpp/common/schemas.cpp +++ b/lib/ocpp/common/schemas.cpp @@ -21,6 +21,18 @@ Schemas::Schemas(fs::path schemas_path) : schemas_path(schemas_path) { } } +Schemas::Schemas(const json& schema_in) : schema(schema_in) { + validator = std::make_shared( + [this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker); + validator->set_root_schema(this->schema); +} + +Schemas::Schemas(json&& schema_in) : schema(std::move(schema_in)) { + validator = std::make_shared( + [this](const json_uri& uri, json& schema) { this->loader(uri, schema); }, Schemas::format_checker); + validator->set_root_schema(this->schema); +} + void Schemas::load_root_schema() { fs::path config_schema_path = this->schemas_path / "Config.json"; diff --git a/lib/ocpp/common/websocket/websocket_libwebsockets.cpp b/lib/ocpp/common/websocket/websocket_libwebsockets.cpp index 33f32a4c1..06293d65e 100644 --- a/lib/ocpp/common/websocket/websocket_libwebsockets.cpp +++ b/lib/ocpp/common/websocket/websocket_libwebsockets.cpp @@ -293,7 +293,7 @@ static int callback_minimal(struct lws* wsi, enum lws_callback_reasons reason, v if (owner not_eq nullptr) { return data->get_owner()->process_callback(wsi, static_cast(reason), user, in, len); } else { - EVLOG_warning << "callback_minimal called, but data->owner is nullptr. Reason: " << reason; + EVLOG_debug << "callback_minimal called, but data->owner is nullptr. Reason: " << reason; } } } @@ -838,7 +838,9 @@ void WebsocketLibwebsockets::reconnect(long delay) { } void WebsocketLibwebsockets::close(const WebsocketCloseReason code, const std::string& reason) { - EVLOG_info << "Closing websocket: " << reason; + if (!reason.empty()) { + EVLOG_info << "Closing websocket: " << reason; + } { std::lock_guard lk(this->reconnect_mutex); diff --git a/lib/ocpp/v16/charge_point.cpp b/lib/ocpp/v16/charge_point.cpp index ee91cafdc..69cc94194 100644 --- a/lib/ocpp/v16/charge_point.cpp +++ b/lib/ocpp/v16/charge_point.cpp @@ -327,14 +327,6 @@ void ChargePoint::register_security_event_callback( this->charge_point->register_security_event_callback(callback); } -GetConfigurationResponse ChargePoint::get_configuration_key(const GetConfigurationRequest& request) { - return this->charge_point->get_configuration_key(request); -} - -ConfigurationStatus ChargePoint::set_custom_configuration_key(CiString<50> key, CiString<500> value) { - return this->charge_point->set_custom_configuration_key(key, value); -} - void ChargePoint::register_is_token_reserved_for_connector_callback( const std::function& callback) { this->charge_point->register_is_token_reserved_for_connector_callback(callback); @@ -351,6 +343,14 @@ void ChargePoint::register_set_display_message_callback( this->charge_point->register_set_display_message_callback(set_display_message_callback); } +GetConfigurationResponse ChargePoint::get_configuration_key(const GetConfigurationRequest& request) { + return this->charge_point->get_configuration_key(request); +} + +ConfigurationStatus ChargePoint::set_custom_configuration_key(CiString<50> key, CiString<500> value) { + return this->charge_point->set_custom_configuration_key(key, value); +} + void ChargePoint::set_message_queue_resume_delay(std::chrono::seconds delay) { this->charge_point->set_message_queue_resume_delay(delay); } diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index 7de14c0cd..d2daba2bc 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -341,11 +341,52 @@ std::vector ChargePointConfiguration::getSupportedCh std::vector supported_purpose_types; const auto str_list = this->config["Internal"]["SupportedChargingProfilePurposeTypes"]; for (const auto& str : str_list) { - supported_purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + try { + supported_purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of SupportedChargingProfilePurposeTypes: " << str; + } } return supported_purpose_types; } +std::vector ChargePointConfiguration::getIgnoredProfilePurposesOffline() { + if (not this->config["Internal"].contains("IgnoredProfilePurposesOffline")) { + return {}; + } + + std::vector purpose_types; + const auto str_list = split_string(this->config["Internal"]["IgnoredProfilePurposesOffline"], ','); + for (const auto& str : str_list) { + try { + purpose_types.push_back(conversions::string_to_charging_profile_purpose_type(str)); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline: " << str; + } + } + return purpose_types; +} + +bool ChargePointConfiguration::setIgnoredProfilePurposesOffline(const std::string& ignored_profile_purposes_offline) { + if (this->getIgnoredProfilePurposesOfflineKeyValue() == std::nullopt) { + return false; + } + + const auto profile_purposes = split_string(ignored_profile_purposes_offline, ','); + for (const auto purpose : profile_purposes) { + try { + conversions::string_to_charging_profile_purpose_type(purpose); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline: " << purpose; + return false; + } + } + + this->config["Internal"]["IgnoredProfilePurposesOffline"] = ignored_profile_purposes_offline; + this->setInUserConfig("Internal", "IgnoredProfilePurposesOffline", ignored_profile_purposes_offline); + return true; +} + int32_t ChargePointConfiguration::getMaxCompositeScheduleDuration() { return this->config["Internal"]["MaxCompositeScheduleDuration"]; } @@ -664,7 +705,31 @@ KeyValue ChargePointConfiguration::getSupportedChargingProfilePurposeTypesKeyVal kv.readonly = true; std::vector purpose_types; for (const auto& entry : this->getSupportedChargingProfilePurposeTypes()) { - purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + try { + purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + } catch (const EnumToStringException& e) { + EVLOG_warning << "Could not convert element of SupportedChargingProfilePurposeTypes to string"; + } + } + kv.value.emplace(to_csl(purpose_types)); + return kv; +} + +std::optional ChargePointConfiguration::getIgnoredProfilePurposesOfflineKeyValue() { + if (not this->config["Internal"].contains("IgnoredProfilePurposesOffline")) { + return std::nullopt; + } + + KeyValue kv; + kv.key = "IgnoredProfilePurposesOffline"; + kv.readonly = false; + std::vector purpose_types; + for (const auto& entry : this->getIgnoredProfilePurposesOffline()) { + try { + purpose_types.push_back(conversions::charging_profile_purpose_type_to_string(entry)); + } catch (const EnumToStringException& e) { + EVLOG_warning << "Could not convert element of IgnoredProfilePurposesOffline to string"; + } } kv.value.emplace(to_csl(purpose_types)); return kv; @@ -2858,20 +2923,28 @@ ConfigurationStatus ChargePointConfiguration::setCustomKey(CiString<50> key, CiS } std::lock_guard lock(this->configuration_mutex); try { - const auto type = this->custom_schema["properties"][key]["type"]; + const auto type = custom_schema["properties"][key]["type"]; + json new_value; if (type == "integer") { - this->config["Custom"][key] = std::stoi(value.get()); + new_value = std::stoi(value.get()); } else if (type == "number") { - this->config["Custom"][key] = std::stof(value.get()); + new_value = std::stof(value.get()); } else if (type == "string" or type == "array") { - this->config["Custom"][key] = value.get(); + new_value = value.get(); } else if (type == "boolean") { - this->config["Custom"][key] = ocpp::conversions::string_to_bool(value.get()); + new_value = ocpp::conversions::string_to_bool(value.get()); } else { return ConfigurationStatus::Rejected; } + + // validate the updated key against the schema + Schemas schema(custom_schema); + json model; + model[key] = new_value; + schema.get_validator()->validate(model); // throws exception on error + config["Custom"][key] = new_value; } catch (const std::exception& e) { - EVLOG_warning << "Could not set custom configuration key"; + EVLOG_warning << "Could not set custom configuration key: " << e.what(); return ConfigurationStatus::Rejected; } @@ -2942,6 +3015,9 @@ std::optional ChargePointConfiguration::get(CiString<50> key) { if (key == "SupportedChargingProfilePurposeTypes") { return this->getSupportedChargingProfilePurposeTypesKeyValue(); } + if (key == "IgnoredProfilePurposesOffline") { + return this->getIgnoredProfilePurposesOfflineKeyValue(); + } if (key == "MaxCompositeScheduleDuration") { return this->getMaxCompositeScheduleDurationKeyValue(); } @@ -3263,6 +3339,11 @@ std::vector ChargePointConfiguration::get_all_key_value() { ConfigurationStatus ChargePointConfiguration::set(CiString<50> key, CiString<500> value) { std::lock_guard lock(this->configuration_mutex); + if (key == "IgnoredProfilePurposesOffline") { + if (this->setIgnoredProfilePurposesOffline(value) == false) { + return ConfigurationStatus::Rejected; + } + } if (key == "AllowOfflineTxForUnknownId") { if (this->getAllowOfflineTxForUnknownId() == std::nullopt) { return ConfigurationStatus::NotSupported; diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index 6c064ddfd..3c9d037cc 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -32,7 +33,6 @@ const auto INITIAL_CERTIFICATE_REQUESTS_DELAY = std::chrono::seconds(60); const auto WEBSOCKET_INIT_DELAY = std::chrono::seconds(2); const auto DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD = 2E5; const auto DEFAULT_BOOT_NOTIFICATION_INTERVAL_S = 60; // fallback interval if BootNotification returns interval of 0. -const auto DEFAULT_WAIT_FOR_FUTURE_TIMEOUT = std::chrono::seconds(60); const auto DEFAULT_PRICE_NUMBER_OF_DECIMALS = 3; ChargePointImpl::ChargePointImpl(const std::string& config, const fs::path& share_path, @@ -60,6 +60,8 @@ ChargePointImpl::ChargePointImpl(const std::string& config, const fs::path& shar this->transaction_handler = std::make_unique(this->configuration->getNumberOfConnectors()); this->external_notify = {v16::MessageType::StartTransactionResponse}; this->message_queue = this->create_message_queue(); + this->message_dispatcher = + std::make_unique(*this->message_queue, *this->configuration, this->registration_status); auto log_formats = this->configuration->getLogMessagesFormat(); bool log_to_console = std::find(log_formats.begin(), log_formats.end(), "console") != log_formats.end(); bool detailed_log_to_console = @@ -292,6 +294,13 @@ void ChargePointImpl::init_websocket() { } this->message_queue->resume(this->message_queue_resume_delay); this->connected_callback(); + + // signal_set_charging_profiles_callback since composite schedule could have changed if + // IgnoredProfilePurposesOffline are configured when becoming online + if (this->signal_set_charging_profiles_callback != nullptr and + not this->configuration->getIgnoredProfilePurposesOffline().empty()) { + this->signal_set_charging_profiles_callback(); + } }); this->websocket->register_disconnected_callback([this]() { if (this->connection_state_changed_callback != nullptr) { @@ -307,6 +316,12 @@ void ChargePointImpl::init_websocket() { if (this->v2g_certificate_timer != nullptr) { this->v2g_certificate_timer->stop(); } + // signal_set_charging_profiles_callback since composite schedule could have changed if + // IgnoredProfilePurposesOffline are configured when becoming offline + if (this->signal_set_charging_profiles_callback != nullptr and + not this->configuration->getIgnoredProfilePurposesOffline().empty()) { + this->signal_set_charging_profiles_callback(); + } }); this->websocket->register_closed_callback([this](const WebsocketCloseReason reason) { if (this->switch_security_profile_callback != nullptr) { @@ -404,7 +419,7 @@ void ChargePointImpl::heartbeat(bool initiated_by_trigger_message) { HeartbeatRequest req; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePointImpl::boot_notification(bool initiated_by_trigger_message) { @@ -421,7 +436,7 @@ void ChargePointImpl::boot_notification(bool initiated_by_trigger_message) { req.meterType = this->configuration->getMeterType(); ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePointImpl::clock_aligned_meter_values_sample() { @@ -962,7 +977,7 @@ void ChargePointImpl::send_meter_value(int32_t connector, MeterValue meter_value req.meterValue.push_back(meter_value); ocpp::Call call(req, message_id); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePointImpl::send_meter_value_on_pricing_trigger(const int32_t connector_number, @@ -1103,6 +1118,8 @@ bool ChargePointImpl::restart(const std::map& connector_ this->database_handler->open_connection(); // instantiating new message queue on restart this->message_queue = this->create_message_queue(); + this->message_dispatcher = + std::make_unique(*this->message_queue, *this->configuration, this->registration_status); return this->start(connector_status_map, bootreason, {}); } else { EVLOG_warning << "Attempting to restart Chargepoint while it has not been stopped before"; @@ -1244,10 +1261,11 @@ void ChargePointImpl::message_callback(const std::string& message) { enhanced_message = this->message_queue->receive(message); } catch (const TimePointParseException& e) { EVLOG_error << "Exception during handling of message: " << e.what(); - this->send(CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({}))); + this->message_dispatcher->dispatch_call_error( + CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({}))); } catch (const json::exception& e) { EVLOG_error << "JSON exception during reception of message: " << e.what(); - this->send(CallError(MessageId("-1"), "GenericError", e.what(), json({}))); + this->message_dispatcher->dispatch_call_error(CallError(MessageId("-1"), "GenericError", e.what(), json({}))); return; } @@ -1260,7 +1278,7 @@ void ChargePointImpl::message_callback(const std::string& message) { // FIXME(kai): however, only send a CALLERROR when it is a CALL message we just received if (enhanced_message.messageTypeId == MessageTypeId::CALL) { auto call_error = CallError(enhanced_message.uniqueId, "NotSupported", "", json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } else if (enhanced_message.messageTypeId == MessageTypeId::CALLERROR) { EVLOG_error << "Received a CALLERROR in response to a " << conversions::messagetype_to_string(enhanced_message.messageType) << ": " << message; @@ -1303,13 +1321,13 @@ void ChargePointImpl::message_callback(const std::string& message) { response.status = RemoteStartStopStatus::Rejected; const ocpp::CallResult call_result(response, enhanced_message.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } else if (enhanced_message.messageType == MessageType::RemoteStopTransaction) { RemoteStopTransactionResponse response; response.status = RemoteStartStopStatus::Rejected; const ocpp::CallResult call_result(response, enhanced_message.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } else { this->handle_message(enhanced_message); } @@ -1329,14 +1347,14 @@ void ChargePointImpl::message_callback(const std::string& message) { EVLOG_error << "JSON exception during handling of message: " << e.what(); if (json_message.is_array() && json_message.size() > MESSAGE_ID) { auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); this->securityEventNotification(ocpp::security_events::INVALIDMESSAGES, std::optional>(message), true); } } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); this->securityEventNotification(ocpp::security_events::INVALIDMESSAGES, std::optional>(message), true); } @@ -1650,7 +1668,7 @@ void ChargePointImpl::handleChangeAvailabilityRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); // if scheduled: execute status transition for connector 0 // if accepted: execute status transition for connector 0 and other connectors @@ -1691,7 +1709,7 @@ void ChargePointImpl::handleChangeConfigurationRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); responded = true; this->websocket->reconnect(1000); } else { @@ -1726,7 +1744,7 @@ void ChargePointImpl::handleChangeConfigurationRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); int32_t security_profile = std::stoi(call.msg.value); responded = true; this->switch_security_profile_callback = [this, security_profile]() { @@ -1781,7 +1799,7 @@ void ChargePointImpl::handleChangeConfigurationRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } if (this->configuration_key_changed_callbacks.count(call.msg.key) and @@ -1832,7 +1850,7 @@ void ChargePointImpl::handleClearCacheRequest(ocpp::Call call } catch (QueryExecutionException& e) { auto call_error = CallError(call.uniqueId, "InternalError", "Database error while clearing authorization cache", json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } } else { @@ -1840,7 +1858,7 @@ void ChargePointImpl::handleClearCacheRequest(ocpp::Call call } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleDataTransferRequest(ocpp::Call call) { @@ -1888,7 +1906,7 @@ void ChargePointImpl::handleDataTransferRequest(ocpp::Call } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleGetConfigurationRequest(ocpp::Call call) { @@ -1896,7 +1914,7 @@ void ChargePointImpl::handleGetConfigurationRequest(ocpp::Callget_configuration_key(call.msg); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Call call) { @@ -1912,7 +1930,7 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } referenced_connectors.push_back(call.msg.connectorId.value()); @@ -1956,7 +1974,7 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -1975,7 +1993,7 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } } @@ -1993,7 +2011,7 @@ void ChargePointImpl::handleRemoteStartTransactionRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (this->configuration->getAuthorizeRemoteTxRequests()) { this->provide_token_callback(call.msg.idTag.get(), referenced_connectors, false); @@ -2046,7 +2064,7 @@ void ChargePointImpl::handleRemoteStopTransactionRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (connector > 0) { this->stop_transaction_callback(connector, Reason::Remote); @@ -2069,7 +2087,7 @@ void ChargePointImpl::handleResetRequest(ocpp::Call call) { // send response ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == ResetStatus::Accepted) { // gracefully stop all transactions and send StopTransaction. Restart software afterwards @@ -2237,7 +2255,7 @@ void ChargePointImpl::handleUnlockConnectorRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleHeartbeatResponse(CallResult call_result) { @@ -2287,7 +2305,7 @@ void ChargePointImpl::handleSetChargingProfileRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == ChargingProfileStatus::Accepted) { if (this->signal_set_charging_profiles_callback != nullptr) { @@ -2335,7 +2353,7 @@ void ChargePointImpl::handleGetCompositeScheduleRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleClearChargingProfileRequest(ocpp::Call call) { @@ -2361,7 +2379,7 @@ void ChargePointImpl::handleClearChargingProfileRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == ClearChargingProfileStatus::Accepted and this->signal_set_charging_profiles_callback != nullptr) { @@ -2403,7 +2421,7 @@ void ChargePointImpl::handleTriggerMessageRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (!valid) { return; @@ -2463,7 +2481,7 @@ void ChargePointImpl::handleGetDiagnosticsRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleUpdateFirmwareRequest(ocpp::Call call) { @@ -2473,7 +2491,7 @@ void ChargePointImpl::handleUpdateFirmwareRequest(ocpp::Callupdate_firmware_callback(call.msg); } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleExtendedTriggerMessageRequest(ocpp::Call call) { @@ -2519,7 +2537,7 @@ void ChargePointImpl::handleExtendedTriggerMessageRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (!valid) { return; @@ -2601,7 +2619,7 @@ void ChargePointImpl::sign_certificate(const ocpp::CertificateSigningUseEnum& ce req.csr = response.csr.value(); ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePointImpl::update_ocsp_cache() { @@ -2646,7 +2664,7 @@ void ChargePointImpl::handleCertificateSignedRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == CertificateSignedStatusEnumType::Rejected) { this->securityEventNotification( @@ -2691,7 +2709,7 @@ void ChargePointImpl::handleGetInstalledCertificateIdsRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleDeleteCertificateRequest(ocpp::Call call) { @@ -2706,7 +2724,7 @@ void ChargePointImpl::handleDeleteCertificateRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleInstallCertificateRequest(ocpp::Call call) { @@ -2731,7 +2749,7 @@ void ChargePointImpl::handleInstallCertificateRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == InstallCertificateStatusEnumType::Rejected) { this->securityEventNotification( @@ -2751,7 +2769,7 @@ void ChargePointImpl::handleGetLogRequest(ocpp::Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleSignedUpdateFirmware(ocpp::Call call) { @@ -2763,11 +2781,11 @@ void ChargePointImpl::handleSignedUpdateFirmware(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } else { response.status = this->signed_update_firmware_callback(call.msg); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } if (response.status == UpdateFirmwareStatusEnumType::InvalidCertificate) { @@ -2800,7 +2818,7 @@ void ChargePointImpl::securityEventNotification(const CiString<50>& event_type, if (critical_security_event and !this->configuration->getDisableSecurityEventNotifications()) { ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } if (triggered_internally and this->security_event_callback != nullptr) { @@ -2821,7 +2839,7 @@ void ChargePointImpl::log_status_notification(UploadLogStatusEnumType status, in this->log_status_request_id = requestId; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePointImpl::signed_firmware_update_status_notification(FirmwareStatusEnumType status, int requestId, @@ -2848,7 +2866,7 @@ void ChargePointImpl::signed_firmware_update_status_notification(FirmwareStatusE this->signed_firmware_status_request_id = requestId; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); if (status == FirmwareStatusEnumType::InvalidSignature) { this->securityEventNotification(ocpp::security_events::INVALIDFIRMWARESIGNATURE, std::nullopt, true); @@ -2874,7 +2892,7 @@ void ChargePointImpl::handleReserveNowRequest(ocpp::Call call } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleCancelReservationRequest(ocpp::Call call) { @@ -2887,7 +2905,7 @@ void ChargePointImpl::handleCancelReservationRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleSendLocalListRequest(ocpp::Call call) { @@ -2941,7 +2959,7 @@ void ChargePointImpl::handleSendLocalListRequest(ocpp::Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handleGetLocalListVersionRequest(ocpp::Call call) { @@ -2963,7 +2981,7 @@ void ChargePointImpl::handleGetLocalListVersionRequest(ocpp::Callsend(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } catch (RequiredEntryNotFoundException& e) { try { @@ -2974,13 +2992,13 @@ void ChargePointImpl::handleGetLocalListVersionRequest(ocpp::Callsend(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } DataTransferResponse ChargePointImpl::handle_set_user_price(const std::optional& msg) { @@ -3202,58 +3220,6 @@ void ChargePointImpl::set_time_offset_timer(const std::string& date_time) { this->change_time_offset_timer->at(d.to_time_point()); } -template bool ChargePointImpl::send(ocpp::Call call, bool initiated_by_trigger_message) { - const auto message_type = conversions::string_to_messagetype(json(call).at(CALL_ACTION)); - const auto message_transmission_priority = get_message_transmission_priority( - is_boot_notification_message(message_type), initiated_by_trigger_message, - (this->registration_status == RegistrationStatus::Accepted), is_transaction_message(message_type), - this->configuration->getQueueAllMessages().value_or(false)); - switch (message_transmission_priority) { - case MessageTransmissionPriority::SendImmediately: - this->message_queue->push(call); - return true; - case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: - this->message_queue->push(call, true); - return true; - case MessageTransmissionPriority::Discard: - return false; - } - throw std::runtime_error("Missing handling for MessageTransmissionPriority"); -} - -template -std::future> ChargePointImpl::send_async(ocpp::Call call, - bool initiated_by_trigger_message) { - const auto message_type = conversions::string_to_messagetype(json(call).at(CALL_ACTION)); - const auto message_transmission_priority = get_message_transmission_priority( - is_boot_notification_message(message_type), initiated_by_trigger_message, - (this->registration_status == RegistrationStatus::Accepted), is_transaction_message(message_type), - this->configuration->getQueueAllMessages().value_or(false)); - - switch (message_transmission_priority) { - case MessageTransmissionPriority::SendImmediately: - return this->message_queue->push_async(call); - case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: - case MessageTransmissionPriority::Discard: - auto promise = std::promise>(); - auto enhanced_message = EnhancedMessage(); - enhanced_message.offline = true; - promise.set_value(enhanced_message); - return promise.get_future(); - } - throw std::runtime_error("Missing handling for MessageTransmissionPriority"); -} - -template bool ChargePointImpl::send(ocpp::CallResult call_result) { - this->message_queue->push(call_result); - return true; -} - -bool ChargePointImpl::send(CallError call_error) { - this->message_queue->push(call_error); - return true; -} - void ChargePointImpl::status_notification(const int32_t connector, const ChargePointErrorCode errorCode, const ChargePointStatus status, const ocpp::DateTime& timestamp, const std::optional>& info, @@ -3269,7 +3235,7 @@ void ChargePointImpl::status_notification(const int32_t connector, const ChargeP request.vendorId = vendor_id; request.vendorErrorCode = vendor_error_code; ocpp::Call call(request, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } // public API for Core profile @@ -3316,7 +3282,7 @@ IdTagInfo ChargePointImpl::authorize_id_token(CiString<20> idTag, const bool aut ocpp::Call call(req, this->message_queue->createMessageId()); - auto authorize_future = this->send_async(call); + auto authorize_future = this->message_dispatcher->dispatch_call_async(call); if (authorize_future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for Authorize.conf future timed out!"; @@ -3353,6 +3319,12 @@ std::map ChargePointImpl::get_all_composite_charging_ const ChargingRateUnit unit) { std::map charging_schedules; + std::set purposes_to_ignore; + + if (not this->websocket->is_connected()) { + const auto purposes_to_ignore_vec = this->configuration->getIgnoredProfilePurposesOffline(); + purposes_to_ignore.insert(purposes_to_ignore_vec.begin(), purposes_to_ignore_vec.end()); + } for (int connector_id = 0; connector_id <= this->configuration->getNumberOfConnectors(); connector_id++) { const auto start_time = ocpp::DateTime(); @@ -3360,7 +3332,7 @@ std::map ChargePointImpl::get_all_composite_charging_ const auto end_time = ocpp::DateTime(start_time.to_time_point() + duration); const auto valid_profiles = - this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id); + this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id, purposes_to_ignore); const auto composite_schedule = this->smart_charging_handler->calculate_composite_schedule( valid_profiles, start_time, end_time, connector_id, unit); charging_schedules[connector_id] = composite_schedule; @@ -3373,6 +3345,12 @@ std::map ChargePointImpl::get_all_enhanced_composite_charging_schedules(const int32_t duration_s, const ChargingRateUnit unit) { std::map charging_schedules; + std::set purposes_to_ignore; + + if (this->websocket == nullptr or not this->websocket->is_connected()) { + const auto purposes_to_ignore_vec = this->configuration->getIgnoredProfilePurposesOffline(); + purposes_to_ignore.insert(purposes_to_ignore_vec.begin(), purposes_to_ignore_vec.end()); + } for (int connector_id = 0; connector_id <= this->configuration->getNumberOfConnectors(); connector_id++) { const auto start_time = ocpp::DateTime(); @@ -3380,7 +3358,7 @@ ChargePointImpl::get_all_enhanced_composite_charging_schedules(const int32_t dur const auto end_time = ocpp::DateTime(start_time.to_time_point() + duration); const auto valid_profiles = - this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id); + this->smart_charging_handler->get_valid_profiles(start_time, end_time, connector_id, purposes_to_ignore); const auto composite_schedule = this->smart_charging_handler->calculate_enhanced_composite_schedule( valid_profiles, start_time, end_time, connector_id, unit); charging_schedules[connector_id] = composite_schedule; @@ -3517,7 +3495,7 @@ ocpp::v201::AuthorizeResponse ChargePointImpl::data_transfer_pnc_authorize( // Send the DataTransfer(Authorize) to the CSMS Call call(req, this->message_queue->createMessageId()); - auto authorize_future = this->send_async(call); + auto authorize_future = this->message_dispatcher->dispatch_call_async(call); if (authorize_future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for DataTransfer.conf(Authorize) future timed out!"; @@ -3599,7 +3577,7 @@ void ChargePointImpl::data_transfer_pnc_sign_certificate() { req.data.emplace(json(csr_req).dump()); Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } void ChargePointImpl::data_transfer_pnc_get_15118_ev_certificate( @@ -3625,7 +3603,7 @@ void ChargePointImpl::data_transfer_pnc_get_15118_ev_certificate( req.data.emplace(json(cert_req).dump()); Call call(req, this->message_queue->createMessageId()); - auto future = this->send_async(call); + auto future = this->message_dispatcher->dispatch_call_async(call); if (future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for DataTransfer.conf(Get15118EVCertificate) future timed out!"; @@ -3675,7 +3653,7 @@ void ChargePointImpl::data_transfer_pnc_get_certificate_status(const ocpp::v201: req.data.emplace(json(cert_status_req).dump()); Call call(req, this->message_queue->createMessageId()); - auto future = this->send_async(call); + auto future = this->message_dispatcher->dispatch_call_async(call); if (future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for DataTransfer.conf(GetCertificateStatus) future timed out!"; @@ -3745,7 +3723,7 @@ void ChargePointImpl::handle_data_transfer_pnc_trigger_message(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == DataTransferStatus::Accepted) { // send sign certificate wrapped in data_transfer @@ -3795,7 +3773,7 @@ void ChargePointImpl::handle_data_transfer_pnc_certificate_signed(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (certificate_response.status == CertificateSignedStatusEnumType::Rejected) { this->securityEventNotification(ocpp::security_events::INVALIDCHARGEPOINTCERTIFICATE, @@ -3804,11 +3782,11 @@ void ChargePointImpl::handle_data_transfer_pnc_certificate_signed(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } catch (const std::exception& e) { EVLOG_error << "Unknown Error while handling DataTransfer message CertificateSigned.req: " << e.what(); CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } } @@ -3858,7 +3836,7 @@ void ChargePointImpl::handle_data_transfer_pnc_get_installed_certificates(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handle_data_transfer_delete_certificate(Call call) { @@ -3888,7 +3866,7 @@ void ChargePointImpl::handle_data_transfer_delete_certificate(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePointImpl::handle_data_transfer_install_certificate(Call call) { @@ -3916,7 +3894,7 @@ void ChargePointImpl::handle_data_transfer_install_certificate(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } std::optional ChargePointImpl::data_transfer(const CiString<255>& vendorId, @@ -3930,7 +3908,7 @@ std::optional ChargePointImpl::data_transfer(const CiStrin DataTransferResponse response; response.status = DataTransferStatus::Rejected; ocpp::Call call(req, this->message_queue->createMessageId()); - auto data_transfer_future = this->send_async(call); + auto data_transfer_future = this->message_dispatcher->dispatch_call_async(call); if (this->websocket == nullptr or !this->websocket->is_connected()) { EVLOG_warning << "Attempting to send DataTransfer.req but charging station is offline"; @@ -4032,7 +4010,7 @@ void ChargePointImpl::start_transaction(std::shared_ptr transaction transaction->set_start_transaction_message_id(message_id.get()); transaction->change_meter_values_sample_interval(this->configuration->getMeterValueSampleInterval()); - this->send(call); + this->message_dispatcher->dispatch_call(call); if (this->transaction_started_callback != nullptr) { this->transaction_started_callback(transaction->get_connector(), transaction->get_session_id()); @@ -4059,11 +4037,8 @@ void ChargePointImpl::on_session_started(int32_t connector, const std::string& s } void ChargePointImpl::on_session_stopped(const int32_t connector, const std::string& session_id) { - // TODO(piet) fix this when evse manager signals clearance of an error - if (this->status->get_state(connector) == ChargePointStatus::Faulted) { - this->status->submit_event(connector, FSMEvent::I1_ReturnToAvailable, ocpp::DateTime()); - } else if (this->status->get_state(connector) != ChargePointStatus::Reserved && - this->status->get_state(connector) != ChargePointStatus::Unavailable) { + if (this->status->get_state(connector) != ChargePointStatus::Reserved && + this->status->get_state(connector) != ChargePointStatus::Unavailable) { this->status->submit_event(connector, FSMEvent::BecomeAvailable, ocpp::DateTime()); } @@ -4200,7 +4175,7 @@ void ChargePointImpl::stop_transaction(int32_t connector, Reason reason, std::op { std::lock_guard lock(this->stop_transaction_mutex); - this->send(call); + this->message_dispatcher->dispatch_call(call); } if (this->transaction_stopped_callback != nullptr) { @@ -4348,7 +4323,7 @@ void ChargePointImpl::diagnostic_status_notification(DiagnosticsStatus status, b this->diagnostics_status = status; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send_async(call, true); + this->message_dispatcher->dispatch_call_async(call, true); } void ChargePointImpl::firmware_status_notification(FirmwareStatus status, bool initiated_by_trigger_message) { @@ -4370,7 +4345,7 @@ void ChargePointImpl::firmware_status_notification(FirmwareStatus status, bool i this->firmware_status = status; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send_async(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call_async(call, initiated_by_trigger_message); if (this->firmware_update_is_pending) { this->change_all_connectors_to_unavailable_for_firmware_update(); diff --git a/lib/ocpp/v16/message_dispatcher.cpp b/lib/ocpp/v16/message_dispatcher.cpp new file mode 100644 index 000000000..fb7067b7f --- /dev/null +++ b/lib/ocpp/v16/message_dispatcher.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include + +namespace ocpp { +namespace v16 { + +void MessageDispatcher::dispatch_call(const json& call, bool triggered) { + const auto message_type = conversions::string_to_messagetype(call.at(CALL_ACTION)); + const auto message_transmission_priority = get_message_transmission_priority( + is_boot_notification_message(message_type), triggered, + (this->registration_status == RegistrationStatus::Accepted), is_transaction_message(message_type), + this->configuration.getQueueAllMessages().value_or(false)); + switch (message_transmission_priority) { + case MessageTransmissionPriority::SendImmediately: + this->message_queue.push_call(call); + return; + case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: + this->message_queue.push_call(call, true); + return; + case MessageTransmissionPriority::Discard: + return; + } + throw std::runtime_error("Missing handling for MessageTransmissionPriority"); +} + +std::future> MessageDispatcher::dispatch_call_async(const json& call, + bool triggered) { + const auto message_type = conversions::string_to_messagetype(call.at(CALL_ACTION)); + const auto message_transmission_priority = get_message_transmission_priority( + is_boot_notification_message(message_type), triggered, + (this->registration_status == RegistrationStatus::Accepted), is_transaction_message(message_type), + this->configuration.getQueueAllMessages().value_or(false)); + + switch (message_transmission_priority) { + case MessageTransmissionPriority::SendImmediately: + return this->message_queue.push_call_async(call); + case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: + case MessageTransmissionPriority::Discard: + auto promise = std::promise>(); + auto enhanced_message = EnhancedMessage(); + enhanced_message.offline = true; + promise.set_value(enhanced_message); + return promise.get_future(); + } + throw std::runtime_error("Missing handling for MessageTransmissionPriority"); +} + +void MessageDispatcher::dispatch_call_result(const json& call_result) { + this->message_queue.push_call_result(call_result); +} + +void MessageDispatcher::dispatch_call_error(const json& call_error) { + this->message_queue.push_call_error(call_error); +} + +} // namespace v16 +} // namespace ocpp diff --git a/lib/ocpp/v16/smart_charging.cpp b/lib/ocpp/v16/smart_charging.cpp index aaff886ae..918b7f948 100644 --- a/lib/ocpp/v16/smart_charging.cpp +++ b/lib/ocpp/v16/smart_charging.cpp @@ -386,16 +386,20 @@ void SmartChargingHandler::clear_all_profiles() { } } -std::vector SmartChargingHandler::get_valid_profiles(const ocpp::DateTime& start_time, - const ocpp::DateTime& end_time, - const int connector_id) { +std::vector +SmartChargingHandler::get_valid_profiles(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, + const int connector_id, + const std::set& purposes_to_ignore) { std::vector valid_profiles; { std::lock_guard lk(charge_point_max_profiles_map_mutex); - for (const auto& [stack_level, profile] : stack_level_charge_point_max_profiles_map) { - valid_profiles.push_back(profile); + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::ChargePointMaxProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : stack_level_charge_point_max_profiles_map) { + valid_profiles.push_back(profile); + } } } @@ -413,26 +417,33 @@ std::vector SmartChargingHandler::get_valid_profiles(const ocpp std::lock_guard lk_txd(tx_default_profiles_map_mutex); std::lock_guard lk_tx(tx_profiles_map_mutex); - for (const auto& [stack_level, profile] : itt->second->stack_level_tx_profiles_map) { - // only include profiles that match the transactionId (when there is one) - bool b_add{false}; - if (profile.transactionId) { - if ((transactionId) && (transactionId.value() == profile.transactionId.value())) { - // there is a session/transaction in progress and the ID matches the profile + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::TxProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : itt->second->stack_level_tx_profiles_map) { + // only include profiles that match the transactionId (when there is one) + bool b_add{false}; + + if (profile.transactionId) { + if ((transactionId) && (transactionId.value() == profile.transactionId.value())) { + // there is a session/transaction in progress and the ID matches the profile + b_add = true; + } + } else { + // profile doesn't have a transaction ID specified b_add = true; } - } else { - // profile doesn't have a transaction ID specified - b_add = true; - } - if (b_add) { - valid_profiles.push_back(profile); + if (b_add) { + valid_profiles.push_back(profile); + } } } - for (const auto& [stack_level, profile] : itt->second->stack_level_tx_default_profiles_map) { - valid_profiles.push_back(profile); + if (std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), + ChargingProfilePurposeType::TxDefaultProfile) == std::end(purposes_to_ignore)) { + for (const auto& [stack_level, profile] : itt->second->stack_level_tx_default_profiles_map) { + valid_profiles.push_back(profile); + } } } } diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 171cc74fd..84752cc13 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -1,10 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest +#include #include #include #include #include +#include #include #include #include @@ -19,7 +21,6 @@ using namespace std::chrono_literals; const auto DEFAULT_MAX_CUSTOMER_INFORMATION_DATA_LENGTH = 51200; const std::string VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL = "internal"; const std::string VARIABLE_ATTRIBUTE_VALUE_SOURCE_CSMS = "csms"; -const auto DEFAULT_WAIT_FOR_FUTURE_TIMEOUT = std::chrono::seconds(60); const auto DEFAULT_PRICE_NUMBER_OF_DECIMALS = 3; using DatabaseException = ocpp::common::DatabaseException; @@ -78,45 +79,15 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct } ChargePoint::ChargePoint(const std::map& evse_connector_structure, - std::unique_ptr device_model_storage, const std::string& ocpp_main_path, - const std::string& core_database_path, const std::string& sql_init_path, - const std::string& message_log_path, const std::shared_ptr evse_security, - const Callbacks& callbacks) : + std::unique_ptr device_model_storage_interface, + const std::string& ocpp_main_path, const std::string& core_database_path, + const std::string& sql_init_path, const std::string& message_log_path, + const std::shared_ptr evse_security, const Callbacks& callbacks) : ChargePoint( - evse_connector_structure, std::make_shared(std::move(device_model_storage)), + evse_connector_structure, std::make_shared(std::move(device_model_storage_interface)), std::make_shared( std::make_unique(fs::path(core_database_path) / "cp.db"), sql_init_path), nullptr /* message_queue initialized in this constructor */, message_log_path, evse_security, callbacks) { - std::set message_types_discard_for_queueing; - try { - const auto message_types_discard_for_queueing_csl = ocpp::split_string( - this->device_model - ->get_optional_value(ControllerComponentVariables::MessageTypesDiscardForQueueing) - .value_or(""), - ','); - std::transform(message_types_discard_for_queueing_csl.begin(), message_types_discard_for_queueing_csl.end(), - std::inserter(message_types_discard_for_queueing, message_types_discard_for_queueing.end()), - [](const std::string element) { return conversions::string_to_messagetype(element); }); - } catch (const StringToEnumException& e) { - EVLOG_warning << "Could not convert configured MessageType value of MessageTypesDiscardForQueueing. Please " - "check you configuration: " - << e.what(); - } catch (...) { - EVLOG_warning << "Could not apply MessageTypesDiscardForQueueing configuration"; - } - - this->message_queue = std::make_unique>( - [this](json message) -> bool { return this->connectivity_manager->send_to_websocket(message.dump()); }, - MessageQueueConfig{ - this->device_model->get_value(ControllerComponentVariables::MessageAttempts), - this->device_model->get_value(ControllerComponentVariables::MessageAttemptInterval), - this->device_model->get_optional_value(ControllerComponentVariables::MessageQueueSizeThreshold) - .value_or(DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD), - this->device_model->get_optional_value(ControllerComponentVariables::QueueAllMessages) - .value_or(false), - message_types_discard_for_queueing, - this->device_model->get_value(ControllerComponentVariables::MessageTimeout)}, - this->database_handler); } ChargePoint::ChargePoint(const std::map& evse_connector_structure, @@ -233,7 +204,7 @@ void ChargePoint::on_firmware_update_status_notification(int32_t request_id, } ocpp::Call call(req, this->message_queue->createMessageId()); - this->send_async(call); + this->message_dispatcher->dispatch_call_async(call); if (req.status == FirmwareStatusEnum::Installed) { std::string firmwareVersionMessage = "New firmware succesfully installed! Version: "; @@ -262,7 +233,7 @@ void ChargePoint::on_firmware_update_status_notification(int32_t request_id, this->firmware_status = FirmwareStatusEnum::InstallScheduled; req.status = firmware_status; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send_async(call); + this->message_dispatcher->dispatch_call_async(call); } this->change_all_connectors_to_unavailable_for_firmware_update(); } @@ -287,7 +258,7 @@ ChargePoint::on_get_15118_ev_certificate_request(const Get15118EVCertificateRequ } EVLOG_debug << "Received Get15118EVCertificateRequest " << request; - auto future_res = this->send_async( + auto future_res = this->message_dispatcher->dispatch_call_async( ocpp::Call(request, this->message_queue->createMessageId())); if (future_res.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { @@ -309,7 +280,7 @@ ChargePoint::on_get_15118_ev_certificate_request(const Get15118EVCertificateRequ } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(response_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return response; } } @@ -1069,7 +1040,7 @@ void ChargePoint::on_log_status_notification(UploadLogStatusEnum status, int32_t this->upload_log_status_id = requestId; ocpp::Call call(request, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } void ChargePoint::on_security_event(const CiString<50>& event_type, const std::optional>& tech_info, @@ -1087,11 +1058,6 @@ void ChargePoint::on_variable_changed(const SetVariableData& set_variable_data) this->handle_variable_changed(set_variable_data); } -bool ChargePoint::send(CallError call_error) { - this->message_queue->push(call_error); - return true; -} - void ChargePoint::initialize(const std::map& evse_connector_structure, const std::string& message_log_path) { this->device_model->check_integrity(evse_connector_structure); @@ -1170,6 +1136,42 @@ void ChargePoint::initialize(const std::map& evse_connector_st this->connectivity_manager->set_websocket_connection_failed_callback( std::bind(&ChargePoint::websocket_connection_failed, this, std::placeholders::_1)); + if (this->message_queue == nullptr) { + std::set message_types_discard_for_queueing; + try { + const auto message_types_discard_for_queueing_csl = ocpp::split_string( + this->device_model + ->get_optional_value(ControllerComponentVariables::MessageTypesDiscardForQueueing) + .value_or(""), + ','); + std::transform(message_types_discard_for_queueing_csl.begin(), message_types_discard_for_queueing_csl.end(), + std::inserter(message_types_discard_for_queueing, message_types_discard_for_queueing.end()), + [](const std::string element) { return conversions::string_to_messagetype(element); }); + } catch (const StringToEnumException& e) { + EVLOG_warning << "Could not convert configured MessageType value of MessageTypesDiscardForQueueing. Please " + "check you configuration: " + << e.what(); + } catch (...) { + EVLOG_warning << "Could not apply MessageTypesDiscardForQueueing configuration"; + } + + this->message_queue = std::make_unique>( + [this](json message) -> bool { return this->connectivity_manager->send_to_websocket(message.dump()); }, + MessageQueueConfig{ + this->device_model->get_value(ControllerComponentVariables::MessageAttempts), + this->device_model->get_value(ControllerComponentVariables::MessageAttemptInterval), + this->device_model->get_optional_value(ControllerComponentVariables::MessageQueueSizeThreshold) + .value_or(DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD), + this->device_model->get_optional_value(ControllerComponentVariables::QueueAllMessages) + .value_or(false), + message_types_discard_for_queueing, + this->device_model->get_value(ControllerComponentVariables::MessageTimeout)}, + this->database_handler); + } + + this->message_dispatcher = + std::make_unique(*this->message_queue, *this->device_model, registration_status); + if (this->callbacks.configure_network_connection_profile_callback.has_value()) { this->connectivity_manager->set_configure_network_connection_profile_callback( this->callbacks.configure_network_connection_profile_callback.value()); @@ -1377,7 +1379,7 @@ void ChargePoint::handle_message(const EnhancedMessage& messa default: if (message.messageTypeId == MessageTypeId::CALL) { const auto call_error = CallError(message.uniqueId, "NotImplemented", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } break; } @@ -1390,7 +1392,8 @@ void ChargePoint::message_callback(const std::string& message) { } catch (const json::exception& e) { this->logging->central_system("Unknown", message); EVLOG_error << "JSON exception during reception of message: " << e.what(); - this->send(CallError(MessageId("-1"), "RpcFrameworkError", e.what(), json({}))); + this->message_dispatcher->dispatch_call_error( + CallError(MessageId("-1"), "RpcFrameworkError", e.what(), json({}))); const auto& security_event = ocpp::security_events::INVALIDMESSAGES; this->security_event_notification_req(CiString<50>(security_event), CiString<255>(message), true, utils::is_critical(security_event)); @@ -1398,7 +1401,7 @@ void ChargePoint::message_callback(const std::string& message) { } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(MessageId("-1"), "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); const auto& security_event = ocpp::security_events::INVALIDMESSAGES; this->security_event_notification_req(CiString<50>(security_event), CiString<255>(message), true, utils::is_critical(security_event)); @@ -1429,14 +1432,14 @@ void ChargePoint::message_callback(const std::string& message) { response.status = RequestStartStopStatusEnum::Rejected; const ocpp::CallResult call_result(response, enhanced_message.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } else if (enhanced_message.messageType == MessageType::RequestStopTransaction) { // Send rejected: B02.FR.05 RequestStopTransactionResponse response; response.status = RequestStartStopStatusEnum::Rejected; const ocpp::CallResult call_result(response, enhanced_message.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } else { std::string const call_error_message = "Received invalid MessageType: " + @@ -1446,7 +1449,7 @@ void ChargePoint::message_callback(const std::string& message) { // B02.FR.09 send CALLERROR SecurityError const auto call_error = CallError(enhanced_message.uniqueId, "SecurityError", call_error_message, json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } } } else if (this->registration_status == RegistrationStatusEnum::Rejected) { @@ -1462,37 +1465,37 @@ void ChargePoint::message_callback(const std::string& message) { "having received an accepted BootNotificationResponse"; EVLOG_warning << error_message; const auto call_error = CallError(enhanced_message.uniqueId, "SecurityError", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } } else { const auto error_message = "Received other message than BootNotificationResponse before " "having received an accepted BootNotificationResponse"; EVLOG_warning << error_message; const auto call_error = CallError(enhanced_message.uniqueId, "SecurityError", "", json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } } } catch (const EvseOutOfRangeException& e) { EVLOG_error << "Exception during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "OccurrenceConstraintViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } catch (const ConnectorOutOfRangeException& e) { EVLOG_error << "Exception during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "OccurrenceConstraintViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } catch (const TimePointParseException& e) { EVLOG_error << "Exception during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } catch (json::exception& e) { EVLOG_error << "JSON exception during handling of message: " << e.what(); if (json_message.is_array() and json_message.size() > MESSAGE_ID) { auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); } } } @@ -1855,16 +1858,23 @@ bool ChargePoint::validate_set_variable(const SetVariableData& set_variable_data const auto network_configuration_priorities = ocpp::split_string(set_variable_data.attributeValue.get(), ','); const auto active_security_profile = this->device_model->get_value(ControllerComponentVariables::SecurityProfile); - for (const auto configuration_slot : network_configuration_priorities) { - try { - auto network_profile_opt = - this->connectivity_manager->get_network_connection_profile(std::stoi(configuration_slot)); - if (!network_profile_opt.has_value()) { + + try { + const auto network_connection_profiles = json::parse( + this->device_model->get_value(ControllerComponentVariables::NetworkConnectionProfiles)); + for (const auto configuration_slot : network_configuration_priorities) { + auto network_profile_it = + std::find_if(network_connection_profiles.begin(), network_connection_profiles.end(), + [configuration_slot](const SetNetworkProfileRequest& network_profile) { + return network_profile.configurationSlot == std::stoi(configuration_slot); + }); + + if (network_profile_it == network_connection_profiles.end()) { EVLOG_warning << "Could not find network profile for configurationSlot: " << configuration_slot; return false; } - auto network_profile = network_profile_opt.value(); + auto network_profile = SetNetworkProfileRequest(*network_profile_it).connectionData; if (network_profile.securityProfile <= active_security_profile) { continue; @@ -1884,10 +1894,14 @@ bool ChargePoint::validate_set_variable(const SetVariableData& set_variable_data << " is >= 2 but no CSMS Root Certifciate is installed"; return false; } - } catch (const std::invalid_argument& e) { - EVLOG_warning << "NetworkConfigurationPriority is not an integer: " << configuration_slot; - return false; } + } catch (const std::invalid_argument& e) { + EVLOG_warning << "NetworkConfigurationPriority contains at least one value which is not an integer: " + << set_variable_data.attributeValue.get(); + return false; + } catch (const json::exception& e) { + EVLOG_warning << "Could not parse NetworkConnectionProfiles or SetNetworkProfileRequest: " << e.what(); + return false; } } return true; @@ -2003,7 +2017,7 @@ void ChargePoint::security_event_notification_req(const CiString<50>& event_type this->logging->security(json(req).dump()); if (critical) { ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } if (triggered_internally and this->callbacks.security_event_callback != nullptr) { this->callbacks.security_event_callback(event_type, tech_info); @@ -2078,7 +2092,7 @@ void ChargePoint::sign_certificate_req(const ocpp::CertificateSigningUseEnum& ce this->awaited_certificate_signing_use_enum = certificate_signing_use; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePoint::boot_notification_req(const BootReasonEnum& reason, const bool initiated_by_trigger_message) { @@ -2098,7 +2112,7 @@ void ChargePoint::boot_notification_req(const BootReasonEnum& reason, const bool req.chargingStation = charging_station; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePoint::notify_report_req(const int request_id, const std::vector& report_data) { @@ -2112,7 +2126,7 @@ void ChargePoint::notify_report_req(const int request_id, const std::vector call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } else { NotifyReportRequestsSplitter splitter{ req, @@ -2120,7 +2134,7 @@ void ChargePoint::notify_report_req(const int request_id, const std::vectormessage_queue->createMessageId(); }}; for (const auto& msg : splitter.create_call_payloads()) { - this->message_queue->push(msg); + this->message_queue->push_call(msg); } } } @@ -2140,7 +2154,7 @@ AuthorizeResponse ChargePoint::authorize_req(const IdToken id_token, const std:: } ocpp::Call call(req, this->message_queue->createMessageId()); - auto future = this->send_async(call); + auto future = this->message_dispatcher->dispatch_call_async(call); if (future.wait_for(DEFAULT_WAIT_FOR_FUTURE_TIMEOUT) == std::future_status::timeout) { EVLOG_warning << "Waiting for DataTransfer.conf(Authorize) future timed out!"; @@ -2159,7 +2173,7 @@ AuthorizeResponse ChargePoint::authorize_req(const IdToken id_token, const std:: } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return response; } } @@ -2173,7 +2187,7 @@ void ChargePoint::status_notification_req(const int32_t evse_id, const int32_t c req.connectorStatus = status; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePoint::heartbeat_req(const bool initiated_by_trigger_message) { @@ -2181,7 +2195,7 @@ void ChargePoint::heartbeat_req(const bool initiated_by_trigger_message) { heartbeat_request_time = std::chrono::steady_clock::now(); ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePoint::transaction_event_req(const TransactionEventEnum& event_type, const DateTime& timestamp, @@ -2237,7 +2251,7 @@ void ChargePoint::transaction_event_req(const TransactionEventEnum& event_type, remote_start_id_per_evse.erase(it); } - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); if (this->callbacks.transaction_event_callback.has_value()) { this->callbacks.transaction_event_callback.value()(req); @@ -2251,7 +2265,7 @@ void ChargePoint::meter_values_req(const int32_t evse_id, const std::vector call(req, this->message_queue->createMessageId()); - this->send(call, initiated_by_trigger_message); + this->message_dispatcher->dispatch_call(call, initiated_by_trigger_message); } void ChargePoint::report_charging_profile_req(const int32_t request_id, const int32_t evse_id, @@ -2265,12 +2279,12 @@ void ChargePoint::report_charging_profile_req(const int32_t request_id, const in req.tbc = tbc; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } void ChargePoint::report_charging_profile_req(const ReportChargingProfilesRequest& req) { ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } void ChargePoint::notify_event_req(const std::vector& events) { @@ -2280,7 +2294,7 @@ void ChargePoint::notify_event_req(const std::vector& events) { req.seqNo = 0; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } void ChargePoint::notify_customer_information_req(const std::string& data, const int32_t request_id) { @@ -2298,7 +2312,7 @@ void ChargePoint::notify_customer_information_req(const std::string& data, const }(); ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); pos += 512; seq_no++; @@ -2342,7 +2356,7 @@ void ChargePoint::handle_certificate_signed_req(Call c } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (result != ocpp::InstallCertificateResult::Accepted) { this->security_event_notification_req("InvalidChargingStationCertificate", @@ -2481,7 +2495,7 @@ void ChargePoint::handle_set_variables_req(Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); // post handling of changed variables after the SetVariables.conf has been queued this->handle_variables_changed(set_variables_response); @@ -2500,7 +2514,7 @@ void ChargePoint::handle_get_variables_req(const EnhancedMessage max_variables_per_message) { // send a CALLERROR const auto call_error = CallError(call.uniqueId, "OccurenceConstraintViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -2508,7 +2522,7 @@ void ChargePoint::handle_get_variables_req(const EnhancedMessage max_bytes_per_message) { // send a CALLERROR const auto call_error = CallError(call.uniqueId, "FormatViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -2516,7 +2530,7 @@ void ChargePoint::handle_get_variables_req(const EnhancedMessageget_variables(msg.getVariableData); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_get_base_report_req(Call call) { @@ -2525,7 +2539,7 @@ void ChargePoint::handle_get_base_report_req(Call call) { response.status = GenericDeviceModelStatusEnum::Accepted; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == GenericDeviceModelStatusEnum::Accepted) { const auto report_data = this->device_model->get_base_report_data(msg.reportBase); @@ -2548,7 +2562,7 @@ void ChargePoint::handle_get_report_req(const EnhancedMessage if (msg.componentVariable.has_value() and msg.componentVariable->size() > max_items_per_message) { // send a CALLERROR const auto call_error = CallError(call.uniqueId, "OccurenceConstraintViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -2556,7 +2570,7 @@ void ChargePoint::handle_get_report_req(const EnhancedMessage if (message.message_size > max_bytes_per_message) { // send a CALLERROR const auto call_error = CallError(call.uniqueId, "FormatViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -2588,7 +2602,7 @@ void ChargePoint::handle_get_report_req(const EnhancedMessage } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == GenericDeviceModelStatusEnum::Accepted) { this->notify_report_req(msg.requestId, report_data); @@ -2604,7 +2618,7 @@ void ChargePoint::handle_set_network_profile_req(Call EVLOG_warning << "No callback registered to validate network profile"; response.status = SetNetworkProfileStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -2613,7 +2627,7 @@ void ChargePoint::handle_set_network_profile_req(Call EVLOG_warning << "CSMS attempted to set a network profile with a lower securityProfile"; response.status = SetNetworkProfileStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -2622,7 +2636,7 @@ void ChargePoint::handle_set_network_profile_req(Call EVLOG_warning << "CSMS attempted to set a network profile that could not be validated."; response.status = SetNetworkProfileStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -2650,11 +2664,10 @@ void ChargePoint::handle_set_network_profile_req(Call ControllerComponentVariables::NetworkConnectionProfiles.variable.value(), AttributeEnum::Actual, network_connection_profiles.dump(), VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL) != SetVariableStatusEnum::Accepted) { - EVLOG_warning - << "CSMS attempted to set a network profile that could not be written to the device model storage"; + EVLOG_warning << "CSMS attempted to set a network profile that could not be written to the device model"; response.status = SetNetworkProfileStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -2668,7 +2681,7 @@ void ChargePoint::handle_set_network_profile_req(Call response.status = SetNetworkProfileStatusEnum::Accepted; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_reset_req(Call call) { @@ -2737,7 +2750,7 @@ void ChargePoint::handle_reset_req(Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); // Reset response is sent, now set evse connectors to unavailable and / or // stop transaction (depending on reset type) @@ -2773,13 +2786,13 @@ void ChargePoint::handle_clear_cache_req(Call call) { } catch (DatabaseException& e) { auto call_error = CallError(call.uniqueId, "InternalError", "Database error while clearing authorization cache", json({}, true)); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_transaction_event_response(const EnhancedMessage& message) { @@ -2874,7 +2887,7 @@ void ChargePoint::handle_get_transaction_status(const Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_unlock_connector(Call call) { @@ -2894,7 +2907,7 @@ void ChargePoint::handle_unlock_connector(Call call) { } ocpp::CallResult call_result(unlock_response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_trigger_message(Call call) { @@ -2996,7 +3009,7 @@ void ChargePoint::handle_trigger_message(Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status != TriggerMessageStatusEnum::Accepted) { return; @@ -3074,7 +3087,7 @@ void ChargePoint::handle_trigger_message(Call call) { } ocpp::Call call(request, this->message_queue->createMessageId()); - this->send(call, true); + this->message_dispatcher->dispatch_call(call, true); } break; case MessageTriggerEnum::FirmwareStatusNotification: { @@ -3093,7 +3106,7 @@ void ChargePoint::handle_trigger_message(Call call) { } ocpp::Call call(request, this->message_queue->createMessageId()); - this->send(call, true); + this->message_dispatcher->dispatch_call(call, true); } break; case MessageTriggerEnum::SignChargingStationCertificate: { @@ -3186,7 +3199,7 @@ void ChargePoint::handle_remote_start_transaction_request(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_remote_stop_transaction_request(Call call) { @@ -3208,7 +3221,7 @@ void ChargePoint::handle_remote_stop_transaction_request(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_change_availability_req(Call call) { @@ -3221,7 +3234,7 @@ void ChargePoint::handle_change_availability_req(Call EVLOG_warning << "CSMS requested ChangeAvailability for invalid evse id or connector id"; response.status = ChangeAvailabilityStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -3253,7 +3266,7 @@ void ChargePoint::handle_change_availability_req(Call // Respond to the CSMS before performing any changes to avoid StatusNotification.req being sent before // the ChangeAvailabilityResponse. ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (!transaction_active) { // No transactions - execute the change now @@ -3301,7 +3314,7 @@ void ChargePoint::handle_costupdated_req(const Call call) { ocpp::CallResult call_result(response, call.uniqueId); if (!is_cost_enabled() or !this->callbacks.set_running_cost_callback.has_value()) { - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -3345,7 +3358,7 @@ void ChargePoint::handle_costupdated_req(const Call call) { this->device_model->get_value(ControllerComponentVariables::TariffCostCtrlrCurrency); this->callbacks.set_running_cost_callback.value()(running_cost, decimals, currency); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); // In OCPP 2.0.1, the chargepoint status trigger is not used. if (!triggers.at_energy_kwh.has_value() and !triggers.at_power_kw.has_value() and !triggers.at_time.has_value()) { @@ -3385,7 +3398,7 @@ void ChargePoint::handle_set_charging_profile_req(Callsend(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -3399,7 +3412,7 @@ void ChargePoint::handle_set_charging_profile_req(CalladditionalInfo->get(); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -3414,7 +3427,7 @@ void ChargePoint::handle_set_charging_profile_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_clear_charging_profile_req(Call call) { @@ -3438,7 +3451,7 @@ void ChargePoint::handle_clear_charging_profile_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_get_charging_profiles_req(Call call) { @@ -3452,7 +3465,7 @@ void ChargePoint::handle_get_charging_profiles_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == GetChargingProfileStatusEnum::NoProfiles) { return; @@ -3508,7 +3521,7 @@ void ChargePoint::handle_get_composite_schedule_req(Callget_composite_schedule_internal(call.msg); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_firmware_update_req(Call call) { @@ -3538,7 +3551,7 @@ void ChargePoint::handle_firmware_update_req(Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if ((response.status == UpdateFirmwareStatusEnum::InvalidCertificate) or (response.status == UpdateFirmwareStatusEnum::RevokedCertificate)) { @@ -3586,7 +3599,7 @@ void ChargePoint::handle_get_installed_certificate_ids_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_install_certificate_req(Call call) { @@ -3608,7 +3621,7 @@ void ChargePoint::handle_install_certificate_req(Call } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_delete_certificate_req(Call call) { @@ -3631,14 +3644,14 @@ void ChargePoint::handle_delete_certificate_req(Call c } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_get_log_req(Call call) { const GetLogResponse response = this->callbacks.get_log_request_callback(call.msg); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_customer_information_req(Call call) { @@ -3658,7 +3671,7 @@ void ChargePoint::handle_customer_information_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == CustomerInformationStatusEnum::Accepted) { std::string data = ""; @@ -3702,7 +3715,7 @@ void ChargePoint::handle_set_monitoring_base_req(Call msg.monitoringBase == MonitoringBaseEnum::FactoryDefault) { try { this->device_model->clear_custom_monitors(); - } catch (const DeviceModelStorageError& e) { + } catch (const DeviceModelError& e) { EVLOG_warning << "Could not clear custom monitors from DB: " << e.what(); response.status = GenericDeviceModelStatusEnum::Rejected; } @@ -3710,7 +3723,7 @@ void ChargePoint::handle_set_monitoring_base_req(Call } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_set_monitoring_level_req(Call call) { @@ -3734,7 +3747,7 @@ void ChargePoint::handle_set_monitoring_level_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_set_variable_monitoring_req(const EnhancedMessage& message) { @@ -3750,24 +3763,24 @@ void ChargePoint::handle_set_variable_monitoring_req(const EnhancedMessage max_items_per_message) { const auto call_error = CallError(call.uniqueId, "OccurenceConstraintViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } if (message.message_size > max_bytes_message) { const auto call_error = CallError(call.uniqueId, "FormatViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } try { response.setMonitoringResult = this->device_model->set_monitors(msg.setMonitoringData); - } catch (const DeviceModelStorageError& e) { + } catch (const DeviceModelError& e) { EVLOG_error << "Set monitors failed:" << e.what(); } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::notify_monitoring_report_req(const int request_id, @@ -3783,7 +3796,7 @@ void ChargePoint::notify_monitoring_report_req(const int request_id, req.tbc = false; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); } else { // Split for larger message sizes int32_t sequence_num = 0; @@ -3809,7 +3822,7 @@ void ChargePoint::notify_monitoring_report_req(const int request_id, req.monitor = sub_data; ocpp::Call call(req, this->message_queue->createMessageId()); - this->send(call); + this->message_dispatcher->dispatch_call(call); sequence_num++; } @@ -3827,7 +3840,7 @@ void ChargePoint::handle_get_monitoring_report_req(Call max_variable_components_per_message) { const auto call_error = CallError(call.uniqueId, "OccurenceConstraintViolation", "", json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } @@ -3842,13 +3855,13 @@ void ChargePoint::handle_get_monitoring_report_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); if (response.status == GenericDeviceModelStatusEnum::Accepted) { // Send the result with splits if required @@ -3862,12 +3875,12 @@ void ChargePoint::handle_clear_variable_monitoring_req(Calldevice_model->clear_monitors(msg.id); - } catch (const DeviceModelStorageError& e) { + } catch (const DeviceModelError& e) { EVLOG_error << "Clear variable monitoring failed:" << e.what(); } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_get_display_message(const Call call) { @@ -3875,7 +3888,7 @@ void ChargePoint::handle_get_display_message(const Callcallbacks.get_display_message_callback.has_value()) { response.status = GetDisplayMessagesStatusEnum::Unknown; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -3900,18 +3913,18 @@ void ChargePoint::handle_get_display_message(const Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } else { response.status = GetDisplayMessagesStatusEnum::Accepted; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } // Send display messages. The response is empty, so we don't have to get that back. // Sending multiple messages is not supported for now, because there is no need to split them up (yet). ocpp::Call request(messages_request, this->message_queue->createMessageId()); - this->send(request); + this->message_dispatcher->dispatch_call(request); } void ChargePoint::handle_set_display_message(const Call call) { @@ -3919,7 +3932,7 @@ void ChargePoint::handle_set_display_message(const Callcallbacks.set_display_message_callback.has_value()) { response.status = DisplayMessageStatusEnum::Rejected; ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } @@ -3983,14 +3996,14 @@ void ChargePoint::handle_set_display_message(const Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); return; } const DisplayMessage message = message_info_to_display_message(call.msg.message); response = this->callbacks.set_display_message_callback.value()({message}); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_clear_display_message(const Call call) { @@ -3999,12 +4012,12 @@ void ChargePoint::handle_clear_display_message(const Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } response = this->callbacks.clear_display_message_callback.value()(call.msg); ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_data_transfer_req(Call call) { @@ -4019,51 +4032,7 @@ void ChargePoint::handle_data_transfer_req(Call call) { } ocpp::CallResult call_result(response, call.uniqueId); - this->send(call_result); -} - -template bool ChargePoint::send(ocpp::Call call, const bool initiated_by_trigger_message) { - const auto message_type = conversions::string_to_messagetype(json(call).at(CALL_ACTION)); - const auto message_transmission_priority = get_message_transmission_priority( - is_boot_notification_message(message_type), initiated_by_trigger_message, - (this->registration_status == RegistrationStatusEnum::Accepted), is_transaction_message(message_type), - this->device_model->get_optional_value(ControllerComponentVariables::QueueAllMessages).value_or(false)); - switch (message_transmission_priority) { - case MessageTransmissionPriority::SendImmediately: - this->message_queue->push(call); - return true; - case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: - this->message_queue->push(call, true); - return true; - case MessageTransmissionPriority::Discard: - return false; - } - throw std::runtime_error("Missing handling for MessageTransmissionPriority"); -} - -template std::future> ChargePoint::send_async(ocpp::Call call) { - const auto message_type = conversions::string_to_messagetype(json(call).at(CALL_ACTION)); - const auto message_transmission_priority = get_message_transmission_priority( - is_boot_notification_message(message_type), false, - (this->registration_status == RegistrationStatusEnum::Accepted), is_transaction_message(message_type), - this->device_model->get_optional_value(ControllerComponentVariables::QueueAllMessages).value_or(false)); - switch (message_transmission_priority) { - case MessageTransmissionPriority::SendImmediately: - return this->message_queue->push_async(call); - case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: - case MessageTransmissionPriority::Discard: - auto promise = std::promise>(); - auto enhanced_message = EnhancedMessage(); - enhanced_message.offline = true; - promise.set_value(enhanced_message); - return promise.get_future(); - } - throw std::runtime_error("Missing handling for MessageTransmissionPriority"); -} - -template bool ChargePoint::send(ocpp::CallResult call_result) { - this->message_queue->push(call_result); - return true; + this->message_dispatcher->dispatch_call_result(call_result); } std::optional ChargePoint::data_transfer_req(const CiString<255>& vendorId, @@ -4082,7 +4051,7 @@ std::optional ChargePoint::data_transfer_req(const DataTra response.status = DataTransferStatusEnum::Rejected; ocpp::Call call(request, this->message_queue->createMessageId()); - auto data_transfer_future = this->send_async(call); + auto data_transfer_future = this->message_dispatcher->dispatch_call_async(call); if (this->connectivity_manager == nullptr or !this->connectivity_manager->is_websocket_connected()) { return std::nullopt; @@ -4101,7 +4070,7 @@ std::optional ChargePoint::data_transfer_req(const DataTra } catch (const EnumConversionException& e) { EVLOG_error << "EnumConversionException during handling of message: " << e.what(); auto call_error = CallError(enhanced_message.uniqueId, "FormationViolation", e.what(), json({})); - this->send(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return std::nullopt; } } @@ -4133,7 +4102,7 @@ void ChargePoint::handle_send_local_authorization_list_req(Calldevice_model->set_read_only_value(local_entries.component, local_entries.variable.value(), AttributeEnum::Actual, std::to_string(entries), VARIABLE_ATTRIBUTE_VALUE_SOURCE_INTERNAL); - } catch (const DeviceModelStorageError& e) { + } catch (const DeviceModelError& e) { EVLOG_warning << "Could not get local list count from database:" << e.what(); } catch (const DatabaseException& e) { EVLOG_warning << "Could not get local list count from database: " << e.what(); @@ -4148,7 +4117,7 @@ void ChargePoint::handle_send_local_authorization_list_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::handle_get_local_authorization_list_version_req(Call call) { @@ -4161,7 +4130,7 @@ void ChargePoint::handle_get_local_authorization_list_version_req(Callsend(call_error); + this->message_dispatcher->dispatch_call_error(call_error); return; } } else { @@ -4169,7 +4138,7 @@ void ChargePoint::handle_get_local_authorization_list_version_req(Call call_result(response, call.uniqueId); - this->send(call_result); + this->message_dispatcher->dispatch_call_result(call_result); } void ChargePoint::scheduled_check_client_certificate_expiration() { @@ -4353,7 +4322,9 @@ void ChargePoint::cache_cleanup_handler() { } } -GetCompositeScheduleResponse ChargePoint::get_composite_schedule_internal(const GetCompositeScheduleRequest& request) { +GetCompositeScheduleResponse +ChargePoint::get_composite_schedule_internal(const GetCompositeScheduleRequest& request, + const std::set& profiles_to_ignore) { GetCompositeScheduleResponse response; response.status = GenericStatusEnum::Rejected; @@ -4367,7 +4338,8 @@ GetCompositeScheduleResponse ChargePoint::get_composite_schedule_internal(const auto start_time = ocpp::DateTime(); auto end_time = ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(request.duration)); - std::vector valid_profiles = this->smart_charging_handler->get_valid_profiles(request.evseId); + std::vector valid_profiles = + this->smart_charging_handler->get_valid_profiles(request.evseId, profiles_to_ignore); auto schedule = this->smart_charging_handler->calculate_composite_schedule( valid_profiles, start_time, end_time, request.evseId, request.chargingRateUnit); @@ -4521,13 +4493,22 @@ ChargePoint::set_variables(const std::vector& set_variable_data } GetCompositeScheduleResponse ChargePoint::get_composite_schedule(const GetCompositeScheduleRequest& request) { - return this->get_composite_schedule_internal(request); + std::set purposes_to_ignore = utils::get_purposes_to_ignore( + this->device_model->get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) + .value_or(""), + this->is_offline()); + return this->get_composite_schedule_internal(request, purposes_to_ignore); } std::vector ChargePoint::get_all_composite_schedules(const int32_t duration_s, const ChargingRateUnitEnum& unit) { std::vector composite_schedules; + std::set purposes_to_ignore = utils::get_purposes_to_ignore( + this->device_model->get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) + .value_or(""), + this->is_offline()); + const auto number_of_evses = this->evse_manager->get_number_of_evses(); // get all composite schedules including the one for evse_id == 0 for (int32_t evse_id = 0; evse_id <= number_of_evses; evse_id++) { @@ -4535,7 +4516,7 @@ std::vector ChargePoint::get_all_composite_schedules(const in request.duration = duration_s; request.evseId = evse_id; request.chargingRateUnit = unit; - auto composite_schedule_response = this->get_composite_schedule_internal(request); + auto composite_schedule_response = this->get_composite_schedule_internal(request, purposes_to_ignore); if (composite_schedule_response.status == GenericStatusEnum::Accepted and composite_schedule_response.schedule.has_value()) { composite_schedules.push_back(composite_schedule_response.schedule.value()); diff --git a/lib/ocpp/v201/connectivity_manager.cpp b/lib/ocpp/v201/connectivity_manager.cpp index 8dd4a1b5c..336c7c6a0 100644 --- a/lib/ocpp/v201/connectivity_manager.cpp +++ b/lib/ocpp/v201/connectivity_manager.cpp @@ -199,9 +199,12 @@ void ConnectivityManager::init_websocket() { } // cache the network profiles on initialization - cache_network_connection_profiles(); + if (!cache_network_connection_profiles()) { + EVLOG_warning << "No network connection profiles configured, aborting websocket connection."; + return; + } - const int config_slot_int = this->network_connection_priorities.at(this->network_configuration_priority); + const int config_slot_int = this->get_active_network_configuration_slot(); const auto network_connection_profile = this->get_network_connection_profile(config_slot_int); // Not const as the iface member can be set by the configure network connection profile callback @@ -424,11 +427,11 @@ void ConnectivityManager::next_network_configuration_priority() { (this->network_configuration_priority + 1) % (this->network_connection_priorities.size()); } -void ConnectivityManager::cache_network_connection_profiles() { +bool ConnectivityManager::cache_network_connection_profiles() { if (!this->network_connection_profiles.empty()) { EVLOG_debug << " Network connection profiles already cached"; - return; + return true; } // get all the network connection profiles from the device model and cache them @@ -442,9 +445,7 @@ void ConnectivityManager::cache_network_connection_profiles() { this->network_connection_priorities.push_back(num); } - if (this->network_connection_priorities.empty()) { - EVLOG_AND_THROW(std::runtime_error("NetworkConfigurationPriority must not be empty")); - } + return !this->network_connection_priorities.empty(); } } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index a4fcbc308..2b564b18f 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -1211,6 +1211,13 @@ const RequiredComponentVariable& ChargingScheduleChargingRateUnit = { "RateUnit", }), }; +const ComponentVariable& IgnoredProfilePurposesOffline = { + ControllerComponents::SmartChargingCtrlr, + std::nullopt, + std::optional({ + "IgnoredProfilePurposesOffline", + }), +}; const ComponentVariable& TariffCostCtrlrAvailableTariff = { ControllerComponents::TariffCostCtrlr, std::nullopt, diff --git a/lib/ocpp/v201/device_model.cpp b/lib/ocpp/v201/device_model.cpp index 28f119458..b409af02c 100644 --- a/lib/ocpp/v201/device_model.cpp +++ b/lib/ocpp/v201/device_model.cpp @@ -219,8 +219,8 @@ bool include_in_summary_inventory(const ComponentVariable& cv, const VariableAtt GetVariableStatusEnum DeviceModel::request_value_internal(const Component& component_id, const Variable& variable_id, const AttributeEnum& attribute_enum, std::string& value, bool allow_write_only) const { - const auto component_it = this->device_model.find(component_id); - if (component_it == this->device_model.end()) { + const auto component_it = this->device_model_map.find(component_id); + if (component_it == this->device_model_map.end()) { EVLOG_debug << "unknown component in " << component_id.name << "." << variable_id.name; return GetVariableStatusEnum::UnknownComponent; } @@ -233,7 +233,7 @@ GetVariableStatusEnum DeviceModel::request_value_internal(const Component& compo return GetVariableStatusEnum::UnknownVariable; } - const auto attribute_opt = this->storage->get_variable_attribute(component_id, variable_id, attribute_enum); + const auto attribute_opt = this->device_model->get_variable_attribute(component_id, variable_id, attribute_enum); if ((not attribute_opt) or (not attribute_opt->value)) { return GetVariableStatusEnum::NotSupportedAttributeType; @@ -253,11 +253,11 @@ SetVariableStatusEnum DeviceModel::set_value(const Component& component, const V const AttributeEnum& attribute_enum, const std::string& value, const std::string& source, bool allow_read_only) { - if (this->device_model.find(component) == this->device_model.end()) { + if (this->device_model_map.find(component) == this->device_model_map.end()) { return SetVariableStatusEnum::UnknownComponent; } - auto variable_map = this->device_model[component]; + auto variable_map = this->device_model_map[component]; if (variable_map.find(variable) == variable_map.end()) { return SetVariableStatusEnum::UnknownVariable; @@ -274,7 +274,7 @@ SetVariableStatusEnum DeviceModel::set_value(const Component& component, const V return SetVariableStatusEnum::Rejected; } - const auto attribute = this->storage->get_variable_attribute(component, variable, attribute_enum); + const auto attribute = this->device_model->get_variable_attribute(component, variable, attribute_enum); if (!attribute.has_value()) { return SetVariableStatusEnum::NotSupportedAttributeType; @@ -287,7 +287,7 @@ SetVariableStatusEnum DeviceModel::set_value(const Component& component, const V } const auto success = - this->storage->set_variable_attribute_value(component, variable, attribute_enum, value, source); + this->device_model->set_variable_attribute_value(component, variable, attribute_enum, value, source); // Only trigger for actual values if ((attribute_enum == AttributeEnum::Actual) && success && variable_listener) { @@ -310,9 +310,9 @@ SetVariableStatusEnum DeviceModel::set_value(const Component& component, const V return success ? SetVariableStatusEnum::Accepted : SetVariableStatusEnum::Rejected; }; -DeviceModel::DeviceModel(std::unique_ptr device_model_storage) : - storage{std::move(device_model_storage)} { - this->device_model = this->storage->get_device_model(); +DeviceModel::DeviceModel(std::unique_ptr device_model_storage_interface) : + device_model{std::move(device_model_storage_interface)} { + this->device_model_map = this->device_model->get_device_model(); } SetVariableStatusEnum DeviceModel::set_read_only_value(const Component& component, const Variable& variable, @@ -328,8 +328,8 @@ SetVariableStatusEnum DeviceModel::set_read_only_value(const Component& componen std::optional DeviceModel::get_variable_meta_data(const Component& component, const Variable& variable) { - if (this->device_model.count(component) and this->device_model.at(component).count(variable)) { - return this->device_model.at(component).at(variable); + if (this->device_model_map.count(component) and this->device_model_map.at(component).count(variable)) { + return this->device_model_map.at(component).at(variable); } else { return std::nullopt; } @@ -338,7 +338,7 @@ std::optional DeviceModel::get_variable_meta_data(const Compon std::vector DeviceModel::get_base_report_data(const ReportBaseEnum& report_base) { std::vector report_data_vec; - for (auto const& [component, variable_map] : this->device_model) { + for (auto const& [component, variable_map] : this->device_model_map) { for (auto const& [variable, variable_meta_data] : variable_map) { ReportData report_data; @@ -347,8 +347,8 @@ std::vector DeviceModel::get_base_report_data(const ReportBaseEnum& ComponentVariable cv = {component, std::nullopt, variable}; - // request the variable attribute from the device model storage - const auto variable_attributes = this->storage->get_variable_attributes(component, variable); + // request the variable attribute from the device model + const auto variable_attributes = this->device_model->get_variable_attributes(component, variable); // iterate over possibly (Actual, Target, MinSet, MaxSet) for (const auto& variable_attribute : variable_attributes) { @@ -381,7 +381,7 @@ DeviceModel::get_custom_report_data(const std::optional>& component_criteria) { std::vector report_data_vec; - for (auto const& [component, variable_map] : this->device_model) { + for (auto const& [component, variable_map] : this->device_model_map) { if (!component_criteria.has_value() or component_criteria_match(component, component_criteria.value())) { for (auto const& [variable, variable_meta_data] : variable_map) { @@ -391,8 +391,8 @@ DeviceModel::get_custom_report_data(const std::optionalstorage->get_variable_attributes(component, variable); + // request the variable attribute from the device model + const auto variable_attributes = this->device_model->get_variable_attributes(component, variable); for (const auto& variable_attribute : variable_attributes) { report_data.variableAttribute.push_back(variable_attribute); @@ -412,12 +412,12 @@ DeviceModel::get_custom_report_data(const std::optional& evse_connector_structure) { EVLOG_debug << "Checking integrity of device model in storage"; try { - this->storage->check_integrity(); + this->device_model->check_integrity(); int32_t nr_evse_components = 0; std::map evse_id_nr_connector_components; - for (const auto& [component, variable_map] : this->device_model) { + for (const auto& [component, variable_map] : this->device_model_map) { if (component.name == "EVSE") { nr_evse_components++; } else if (component.name == "Connector") { @@ -431,34 +431,33 @@ void DeviceModel::check_integrity(const std::map& evse_connect // check if number of EVSE in the device model matches the configured number if (nr_evse_components != evse_connector_structure.size()) { - throw DeviceModelStorageError("Number of EVSE configured in device model is incompatible with number of " - "configured EVSEs of the ChargePoint"); + throw DeviceModelError("Number of EVSE configured in device model is incompatible with number of " + "configured EVSEs of the ChargePoint"); } for (const auto [evse_id, nr_of_connectors] : evse_connector_structure) { // check if number of Cpnnectors for this EVSE in the device model matches the configured number if (evse_id_nr_connector_components[evse_id] != nr_of_connectors) { - throw DeviceModelStorageError( - "Number of Connectors configured in device model is incompatible with number " - "of configured Connectors of the ChargePoint"); + throw DeviceModelError("Number of Connectors configured in device model is incompatible with number " + "of configured Connectors of the ChargePoint"); } // check if all relevant EVSE and Connector components can be found EVSE evse = {evse_id}; Component evse_component = {"EVSE", std::nullopt, evse}; - if (!this->device_model.count(evse_component)) { - throw DeviceModelStorageError("Could not find required EVSE component in device model"); + if (!this->device_model_map.count(evse_component)) { + throw DeviceModelError("Could not find required EVSE component in device model"); } for (size_t connector_id = 1; connector_id <= nr_of_connectors; connector_id++) { evse_component.name = "Connector"; evse_component.evse.value().connectorId = connector_id; - if (!this->device_model.count(evse_component)) { - throw DeviceModelStorageError("Could not find required Connector component in device model"); + if (!this->device_model_map.count(evse_component)) { + throw DeviceModelError("Could not find required Connector component in device model"); } } } - } catch (const DeviceModelStorageError& e) { - EVLOG_error << "Integrity check in Device Model storage failed:" << e.what(); + } catch (const DeviceModelError& e) { + EVLOG_error << "Integrity check in Device Model failed:" << e.what(); throw e; } } @@ -468,7 +467,7 @@ bool DeviceModel::update_monitor_reference(int32_t monitor_id, const std::string VariableMonitoringMeta* monitor_meta = nullptr; // See if this is a trivial delta monitor and that it exists - for (auto& [component, variable_map] : this->device_model) { + for (auto& [component, variable_map] : this->device_model_map) { bool found_monitor_id = false; for (auto& [variable, variable_meta_data] : variable_map) { @@ -500,7 +499,7 @@ bool DeviceModel::update_monitor_reference(int32_t monitor_id, const std::string if (found_monitor) { try { - if (this->storage->update_monitoring_reference(monitor_id, reference_value)) { + if (this->device_model->update_monitoring_reference(monitor_id, reference_value)) { // Update value in-memory too monitor_meta->reference_value = reference_value; return true; @@ -510,7 +509,7 @@ bool DeviceModel::update_monitor_reference(int32_t monitor_id, const std::string } } catch (const DatabaseException& e) { EVLOG_error << "Exception while updating trivial delta monitor reference with ID: " << monitor_id; - throw DeviceModelStorageError(e.what()); + throw DeviceModelError(e.what()); } } else { EVLOG_warning << "Could not find trivial delta monitor with ID: " << monitor_id @@ -546,7 +545,7 @@ std::vector DeviceModel::set_monitors(const std::vectordevice_model) { + for (const auto& [component, variable_map] : this->device_model_map) { for (const auto& [variable, variable_meta] : variable_map) { if (variable_meta.monitors.find(request.id.value()) != std::end(variable_meta.monitors)) { id_found = true; @@ -566,9 +565,9 @@ std::vector DeviceModel::set_monitors(const std::vectordevice_model.find(request.component); + auto component_it = this->device_model_map.find(request.component); - if (component_it == this->device_model.end()) { + if (component_it == this->device_model_map.end()) { // N04.FR.16 if (request_has_id && id_found) { result.status = SetMonitoringStatusEnum::Rejected; @@ -580,7 +579,7 @@ std::vector DeviceModel::set_monitors(const std::vectordevice_model[request.component]; + auto& variable_map = this->device_model_map[request.component]; auto variable_it = variable_map.find(request.variable); if (variable_it == variable_map.end()) { @@ -655,14 +654,14 @@ std::vector DeviceModel::set_monitors(const std::vectorstorage->set_monitoring_data(request, type); + auto monitor_meta = this->device_model->set_monitoring_data(request, type); if (monitor_meta.has_value()) { // N07.FR.11 // In case of an existing monitor update if (request_has_id && monitor_update_listener) { - auto attribute = this->storage->get_variable_attribute(component_it->first, variable_it->first, - AttributeEnum::Actual); + auto attribute = this->device_model->get_variable_attribute(component_it->first, variable_it->first, + AttributeEnum::Actual); if (attribute.has_value()) { static std::string empty_value{}; @@ -686,7 +685,7 @@ std::vector DeviceModel::set_monitors(const std::vector DeviceModel::set_monitors(const std::vector DeviceModel::get_periodic_monitors() { std::vector periodics; - for (const auto& [component, variable_map] : this->device_model) { + for (const auto& [component, variable_map] : this->device_model_map) { for (const auto& [variable, variable_metadata] : variable_map) { std::vector monitors; @@ -725,11 +724,11 @@ std::vector DeviceModel::get_monitors(const std::vectordevice_model.find(component_variable.component) == this->device_model.end()) { + if (this->device_model_map.find(component_variable.component) == this->device_model_map.end()) { continue; } - auto& variable_map = this->device_model[component_variable.component]; + auto& variable_map = this->device_model_map[component_variable.component]; // N02.FR.16 - if variable is missing, report all existing variables inside that component if (component_variable.variable.has_value() == false) { @@ -777,7 +776,7 @@ std::vector DeviceModel::get_monitors(const std::vectordevice_model) { + for (const auto& [component, variable_map] : this->device_model_map) { for (const auto& [variable, variable_metadata] : variable_map) { std::vector monitors; @@ -811,10 +810,10 @@ std::vector DeviceModel::clear_monitors(const std::vector clear_monitor_res.id = id; try { - auto clear_result = this->storage->clear_variable_monitor(id, allow_protected); + auto clear_result = this->device_model->clear_variable_monitor(id, allow_protected); if (clear_result == ClearMonitoringStatusEnum::Accepted) { // Clear from memory too - for (auto& [component, variable_map] : this->device_model) { + for (auto& [component, variable_map] : this->device_model_map) { for (auto& [variable, variable_metadata] : variable_map) { variable_metadata.monitors.erase(static_cast(id)); } @@ -824,7 +823,7 @@ std::vector DeviceModel::clear_monitors(const std::vector clear_monitor_res.status = clear_result; } catch (const DatabaseException& e) { EVLOG_error << "Clear monitors failed:" << e.what(); - throw DeviceModelStorageError(e.what()); + throw DeviceModelError(e.what()); } clear_monitors_vec.push_back(clear_monitor_res); @@ -835,10 +834,10 @@ std::vector DeviceModel::clear_monitors(const std::vector int32_t DeviceModel::clear_custom_monitors() { try { - int32_t deleted = this->storage->clear_custom_variable_monitors(); + int32_t deleted = this->device_model->clear_custom_variable_monitors(); // Clear from memory too - for (auto& [component, variable_map] : this->device_model) { + for (auto& [component, variable_map] : this->device_model_map) { for (auto& [variable, variable_metadata] : variable_map) { // Delete while iterating all custom monitors for (auto it = variable_metadata.monitors.begin(); it != variable_metadata.monitors.end();) { @@ -854,7 +853,7 @@ int32_t DeviceModel::clear_custom_monitors() { return deleted; } catch (const DatabaseException& e) { EVLOG_error << "Clear custom monitors failed:" << e.what(); - throw DeviceModelStorageError(e.what()); + throw DeviceModelError(e.what()); } return 0; diff --git a/lib/ocpp/v201/device_model_storage_sqlite.cpp b/lib/ocpp/v201/device_model_storage_sqlite.cpp index 90aff34e9..0d6146506 100644 --- a/lib/ocpp/v201/device_model_storage_sqlite.cpp +++ b/lib/ocpp/v201/device_model_storage_sqlite.cpp @@ -22,8 +22,7 @@ DeviceModelStorageSqlite::DeviceModelStorageSqlite(const fs::path& db_path, cons const fs::path& config_path, const bool init_db) { if (init_db) { if (db_path.empty() || migration_files_path.empty() || config_path.empty()) { - EVLOG_AND_THROW( - DeviceModelStorageError("Can not initialize device model storage: one of the paths is empty.")); + EVLOG_AND_THROW(DeviceModelError("Can not initialize device model storage: one of the paths is empty.")); } InitDeviceModelDb init_device_model_db(db_path, migration_files_path); init_device_model_db.initialize_database(config_path, false); @@ -97,7 +96,7 @@ DeviceModelMap DeviceModelStorageSqlite::get_device_model() { std::string select_query = "SELECT c.NAME, c.EVSE_ID, c.CONNECTOR_ID, c.INSTANCE, v.NAME, v.INSTANCE, vc.DATATYPE_ID, " - "vc.SUPPORTS_MONITORING, vc.UNIT, vc.MIN_LIMIT, vc.MAX_LIMIT, vc.VALUES_LIST " + "vc.SUPPORTS_MONITORING, vc.UNIT, vc.MIN_LIMIT, vc.MAX_LIMIT, vc.VALUES_LIST, v.SOURCE " "FROM COMPONENT c " "JOIN VARIABLE v ON c.ID = v.COMPONENT_ID " "JOIN VARIABLE_CHARACTERISTICS vc ON vc.VARIABLE_ID = v.ID"; @@ -130,6 +129,7 @@ DeviceModelMap DeviceModelStorageSqlite::get_device_model() { } VariableCharacteristics characteristics; + VariableMetaData meta_data; characteristics.dataType = static_cast(select_stmt->column_int(6)); characteristics.supportsMonitoring = select_stmt->column_int(7) != 0; @@ -149,7 +149,10 @@ DeviceModelMap DeviceModelStorageSqlite::get_device_model() { characteristics.valuesList = select_stmt->column_text(11); } - VariableMetaData meta_data; + if (select_stmt->column_type(12) != SQLITE_NULL) { + meta_data.source = select_stmt->column_text(12); + } + meta_data.characteristics = characteristics; // Query all monitors for this variable @@ -481,7 +484,7 @@ void DeviceModelStorageSqlite::check_integrity() { << "/" << select_stmt->column_text_nullable(4).value_or("") << ")" << std::endl; } while (select_stmt->step() == SQLITE_ROW); - throw DeviceModelStorageError(error.str()); + throw DeviceModelError(error.str()); } } diff --git a/lib/ocpp/v201/enums.cpp b/lib/ocpp/v201/enums.cpp index 0293a9ba6..939ad3551 100644 --- a/lib/ocpp/v201/enums.cpp +++ b/lib/ocpp/v201/enums.cpp @@ -23,6 +23,7 @@ VariableMonitorType string_to_variable_monitor_type(const std::string& s) { throw std::out_of_range("Provided string " + s + " could not be converted to enum of type VariableMonitorType"); } + } // namespace conversions -} // namespace ocpp::v201 \ No newline at end of file +} // namespace ocpp::v201 diff --git a/lib/ocpp/v201/init_device_model_db.cpp b/lib/ocpp/v201/init_device_model_db.cpp index d236eed65..0b36a1be8 100644 --- a/lib/ocpp/v201/init_device_model_db.cpp +++ b/lib/ocpp/v201/init_device_model_db.cpp @@ -362,14 +362,14 @@ void InitDeviceModelDb::update_variable_characteristics(const VariableCharacteri void InitDeviceModelDb::insert_variable(const DeviceModelVariable& variable, const uint64_t& component_id) { static const std::string statement = - "INSERT OR REPLACE INTO VARIABLE (NAME, INSTANCE, COMPONENT_ID, REQUIRED) VALUES " - "(@name, @instance, @component_id, @required)"; + "INSERT OR REPLACE INTO VARIABLE (NAME, INSTANCE, COMPONENT_ID, REQUIRED, SOURCE) VALUES " + "(@name, @instance, @component_id, @required, @source)"; std::unique_ptr insert_variable_statement; try { insert_variable_statement = this->database->new_statement(statement); - } catch (const common::QueryExecutionException&) { - throw InitDeviceModelDbError("Could not create statement " + statement); + } catch (const common::QueryExecutionException& e) { + throw InitDeviceModelDbError("Could not create statement " + statement + ": " + e.what()); } insert_variable_statement->bind_text("@name", variable.name, ocpp::common::SQLiteString::Transient); @@ -385,6 +385,12 @@ void InitDeviceModelDb::insert_variable(const DeviceModelVariable& variable, con const uint8_t required_int = (variable.required ? 1 : 0); insert_variable_statement->bind_int("@required", required_int); + if (variable.source.has_value()) { + insert_variable_statement->bind_text("@source", variable.source.value()); + } else { + insert_variable_statement->bind_null("@source"); + } + if (insert_variable_statement->step() != SQLITE_DONE) { throw InitDeviceModelDbError("Variable " + variable.name + " could not be inserted: " + std::string(this->database->get_error_message())); @@ -405,8 +411,8 @@ void InitDeviceModelDb::update_variable(const DeviceModelVariable& variable, con } static const std::string update_variable_statement = - "UPDATE VARIABLE SET NAME=@name, INSTANCE=@instance, COMPONENT_ID=@component_id, REQUIRED=@required WHERE " - "ID=@variable_id"; + "UPDATE VARIABLE SET NAME=@name, INSTANCE=@instance, COMPONENT_ID=@component_id, REQUIRED=@required, " + "SOURCE=@source WHERE ID=@variable_id"; std::unique_ptr update_statement; try { @@ -428,6 +434,12 @@ void InitDeviceModelDb::update_variable(const DeviceModelVariable& variable, con const uint8_t required_int = (variable.required ? 1 : 0); update_statement->bind_int("@required", required_int); + if (variable.source.has_value()) { + update_statement->bind_text("@source", variable.source.value(), ocpp::common::SQLiteString::Transient); + } else { + update_statement->bind_null("@source"); + } + if (update_statement->step() != SQLITE_DONE) { throw InitDeviceModelDbError("Could not update variable " + variable.name + ": " + std::string(this->database->get_error_message())); @@ -804,7 +816,8 @@ std::map> InitDeviceModelDb::get_ "c.ID, c.NAME, c.INSTANCE, c.EVSE_ID, c.CONNECTOR_ID, " "v.ID, v.NAME, v.INSTANCE, v.REQUIRED, " "vc.ID, vc.DATATYPE_ID, vc.MAX_LIMIT, vc.MIN_LIMIT, vc.SUPPORTS_MONITORING, vc.UNIT, vc.VALUES_LIST, " - "va.ID, va.MUTABILITY_ID, va.PERSISTENT, va.CONSTANT, va.TYPE_ID, va.VALUE, va.VALUE_SOURCE " + "va.ID, va.MUTABILITY_ID, va.PERSISTENT, va.CONSTANT, va.TYPE_ID, va.VALUE, va.VALUE_SOURCE," + "v.SOURCE " "FROM " "COMPONENT c " "JOIN VARIABLE v ON v.COMPONENT_ID = c.ID " @@ -895,6 +908,14 @@ std::map> InitDeviceModelDb::get_ attribute.variable_attribute.value = select_statement->column_text_nullable(21); attribute.value_source = select_statement->column_text_nullable(22); + if (select_statement->column_type(23) != SQLITE_NULL) { + try { + variable->source = select_statement->column_text(23); + } catch (const std::out_of_range& e) { + EVLOG_error << e.what() << ": Variable Source will not be set (so default will be used)"; + } + } + variable->attributes.push_back(attribute); // Query all monitors @@ -1209,6 +1230,10 @@ void from_json(const json& j, DeviceModelVariable& c) { c.default_actual_value = get_string_value_from_json(default_value); } + if (j.contains("source")) { + c.source = j.at("source"); + } + if (j.contains("monitors")) { if (!c.characteristics.supportsMonitoring) { const std::string error = @@ -1284,6 +1309,11 @@ static void check_integrity(const std::map check_integrity_required_value(const DeviceModelVariable& variable) { + // Required value has a different source so it is correct that the value is not set here. + if (variable.source.has_value() && variable.source != "OCPP") { + return std::nullopt; + } + // For now, we assume that if a variable is required, it should have an 'Actual' value. But the spec is not clear // about this. There are some implicit signs in favor of having always at least an 'Actual' value, but it is not // explicitly stated. Robert asked OCA about this. diff --git a/lib/ocpp/v201/message_dispatcher.cpp b/lib/ocpp/v201/message_dispatcher.cpp new file mode 100644 index 000000000..df8be9e44 --- /dev/null +++ b/lib/ocpp/v201/message_dispatcher.cpp @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include + +namespace ocpp { +namespace v201 { + +void MessageDispatcher::dispatch_call(const json& call, bool triggered) { + const auto message_type = conversions::string_to_messagetype(call.at(CALL_ACTION)); + const auto message_transmission_priority = get_message_transmission_priority( + is_boot_notification_message(message_type), triggered, + (this->registration_status == RegistrationStatusEnum::Accepted), is_transaction_message(message_type), + this->device_model.get_optional_value(ControllerComponentVariables::QueueAllMessages).value_or(false)); + switch (message_transmission_priority) { + case MessageTransmissionPriority::SendImmediately: + this->message_queue.push_call(call); + return; + case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: + this->message_queue.push_call(call, true); + return; + case MessageTransmissionPriority::Discard: + return; + } + throw std::runtime_error("Missing handling for MessageTransmissionPriority"); +} + +std::future> MessageDispatcher::dispatch_call_async(const json& call, + bool triggered) { + const auto message_type = conversions::string_to_messagetype(call.at(CALL_ACTION)); + const auto message_transmission_priority = get_message_transmission_priority( + is_boot_notification_message(message_type), false, + (this->registration_status == RegistrationStatusEnum::Accepted), is_transaction_message(message_type), + this->device_model.get_optional_value(ControllerComponentVariables::QueueAllMessages).value_or(false)); + switch (message_transmission_priority) { + case MessageTransmissionPriority::SendImmediately: + return this->message_queue.push_call_async(call); + case MessageTransmissionPriority::SendAfterRegistrationStatusAccepted: + case MessageTransmissionPriority::Discard: + auto promise = std::promise>(); + auto enhanced_message = EnhancedMessage(); + enhanced_message.offline = true; + promise.set_value(enhanced_message); + return promise.get_future(); + } + throw std::runtime_error("Missing handling for MessageTransmissionPriority"); +} + +void MessageDispatcher::dispatch_call_result(const json& call_result) { + this->message_queue.push_call_result(call_result); +} + +void MessageDispatcher::dispatch_call_error(const json& call_error) { + this->message_queue.push_call_error(call_error); +} + +} // namespace v201 +} // namespace ocpp diff --git a/lib/ocpp/v201/monitoring_updater.cpp b/lib/ocpp/v201/monitoring_updater.cpp index d1e7594f9..df08bdb3a 100644 --- a/lib/ocpp/v201/monitoring_updater.cpp +++ b/lib/ocpp/v201/monitoring_updater.cpp @@ -209,9 +209,8 @@ void MonitoringUpdater::evaluate_monitor(const VariableMonitoringMeta& monitor_m const VariableAttribute& attribute, const std::string& value_previous, const std::string& value_current) { // Don't care about periodic - switch (monitor_meta.monitor.type) { - case MonitorEnum::Periodic: - case MonitorEnum::PeriodicClockAligned: + if (monitor_meta.monitor.type == MonitorEnum::Periodic or + monitor_meta.monitor.type == MonitorEnum::PeriodicClockAligned) { return; } @@ -265,7 +264,7 @@ void MonitoringUpdater::evaluate_monitor(const VariableMonitoringMeta& monitor_m if (!this->device_model->update_monitor_reference(monitor_id, value_current)) { EVLOG_warning << "Could not update delta monitor: " << monitor_id << " reference!"; } - } catch (const DeviceModelStorageError& e) { + } catch (const DeviceModelError& e) { EVLOG_error << "Could not update delta monitor reference with exception: " << e.what(); } } diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 206c85b71..7cc337999 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -459,12 +459,16 @@ SmartChargingHandler::get_reported_profiles(const GetChargingProfilesRequest& re return this->database_handler->get_charging_profiles_matching_criteria(request.evseId, request.chargingProfile); } -std::vector SmartChargingHandler::get_valid_profiles_for_evse(int32_t evse_id) { +std::vector +SmartChargingHandler::get_valid_profiles_for_evse(int32_t evse_id, + const std::set& purposes_to_ignore) { std::vector valid_profiles; auto evse_profiles = this->database_handler->get_charging_profiles_for_evse(evse_id); for (auto profile : evse_profiles) { - if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid) { + if (this->conform_and_validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid and + std::find(std::begin(purposes_to_ignore), std::end(purposes_to_ignore), profile.chargingProfilePurpose) == + std::end(purposes_to_ignore)) { valid_profiles.push_back(profile); } } @@ -472,11 +476,13 @@ std::vector SmartChargingHandler::get_valid_profiles_for_evse(i return valid_profiles; } -std::vector SmartChargingHandler::get_valid_profiles(int32_t evse_id) { - std::vector valid_profiles = get_valid_profiles_for_evse(evse_id); +std::vector +SmartChargingHandler::get_valid_profiles(int32_t evse_id, + const std::set& purposes_to_ignore) { + std::vector valid_profiles = get_valid_profiles_for_evse(evse_id, purposes_to_ignore); if (evse_id != STATION_WIDE_ID) { - auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID); + auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID, purposes_to_ignore); valid_profiles.insert(valid_profiles.end(), station_wide_profiles.begin(), station_wide_profiles.end()); } diff --git a/lib/ocpp/v201/utils.cpp b/lib/ocpp/v201/utils.cpp index 43758fd01..e9697e3f2 100644 --- a/lib/ocpp/v201/utils.cpp +++ b/lib/ocpp/v201/utils.cpp @@ -192,6 +192,24 @@ bool is_critical(const std::string& security_event) { return false; } +std::set get_purposes_to_ignore(const std::string& csl, const bool is_offline) { + if (not is_offline or csl.empty()) { + return {}; + } + + std::set purposes_to_ignore; + const auto purposes_to_ignore_vec = split_string(csl, ','); + + for (const auto purpose : purposes_to_ignore_vec) { + try { + purposes_to_ignore.insert(conversions::string_to_charging_profile_purpose_enum(purpose)); + } catch (std::out_of_range& e) { + EVLOG_warning << "Error while converting charging profile purpose to ignore: " << purpose; + } + } + return purposes_to_ignore; +} + } // namespace utils } // namespace v201 } // namespace ocpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5e188c69..01e7b40c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -88,3 +88,17 @@ endif() if(LIBOCPP_ENABLE_V201) add_subdirectory(lib/ocpp/v201) endif() + +setup_target_for_coverage_gcovr_html( + NAME ${PROJECT_NAME}_gcovr_coverage + EXECUTABLE ctest + DEPENDENCIES libocpp_unit_tests + EXCLUDE "src/*" "tests/*" +) + +setup_target_for_coverage_gcovr_xml( + NAME ${PROJECT_NAME}_gcovr_coverage_xml + EXECUTABLE ctest + DEPENDENCIES libocpp_unit_tests + EXCLUDE "src/*" "tests/*" +) diff --git a/tests/config/v201/resources/component_config/standardized/UnitTestCtrlr.json b/tests/config/v201/resources/component_config/standardized/UnitTestCtrlr.json index e282268b6..67f3a5b4f 100644 --- a/tests/config/v201/resources/component_config/standardized/UnitTestCtrlr.json +++ b/tests/config/v201/resources/component_config/standardized/UnitTestCtrlr.json @@ -8,6 +8,7 @@ "properties": { "UnitTestPropertyA": { "variable_name": "UnitTestPropertyAName", + "source": "OCPP", "characteristics": { "supportsMonitoring": true, "dataType": "boolean" diff --git a/tests/lib/ocpp/common/test_message_queue.cpp b/tests/lib/ocpp/common/test_message_queue.cpp index dbf7f5f21..67e00131f 100644 --- a/tests/lib/ocpp/common/test_message_queue.cpp +++ b/tests/lib/ocpp/common/test_message_queue.cpp @@ -202,7 +202,7 @@ class MessageQueueTest : public ::testing::Test { call.msg.type = message_type; call.msg.data = identifier; call.uniqueId = identifier; - message_queue->push(call); + message_queue->push_call(call); return identifier; } @@ -246,7 +246,7 @@ TEST_F(MessageQueueTest, test_transactional_message_is_sent) { call.msg.type = TestMessageType::TRANSACTIONAL; call.msg.data = "test_data"; call.uniqueId = "0"; - message_queue->push(call); + message_queue->push_call(call); wait_for_calls(); } @@ -261,7 +261,7 @@ TEST_F(MessageQueueTest, test_non_transactional_message_is_sent) { call.msg.type = TestMessageType::NON_TRANSACTIONAL; call.msg.data = "test_data"; call.uniqueId = "0"; - message_queue->push(call); + message_queue->push_call(call); wait_for_calls(); } diff --git a/tests/lib/ocpp/v16/CMakeLists.txt b/tests/lib/ocpp/v16/CMakeLists.txt index ec7864a10..8ac06fd0c 100644 --- a/tests/lib/ocpp/v16/CMakeLists.txt +++ b/tests/lib/ocpp/v16/CMakeLists.txt @@ -1,4 +1,4 @@ -target_include_directories(libocpp_unit_tests PUBLIC +target_include_directories(libocpp_unit_tests PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) @@ -13,6 +13,7 @@ target_sources(libocpp_unit_tests PRIVATE test_message_queue.cpp test_charge_point_state_machine.cpp test_composite_schedule.cpp + test_config_validation.cpp ) # Copy the json files used for testing to the destination directory diff --git a/tests/lib/ocpp/v16/test_config_validation.cpp b/tests/lib/ocpp/v16/test_config_validation.cpp new file mode 100644 index 000000000..f661a0a7b --- /dev/null +++ b/tests/lib/ocpp/v16/test_config_validation.cpp @@ -0,0 +1,126 @@ + +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include + +#include +#include + +#include +#include +#include + +namespace { +using nlohmann::basic_json; +using nlohmann::json; +using nlohmann::json_uri; +using nlohmann::json_schema::basic_error_handler; +using nlohmann::json_schema::json_validator; + +constexpr const char* test_schema = R"({ + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "Json schema for Custom configuration keys", + "$comment": "This is just an example schema and can be modified according to custom requirements", + "type": "object", + "required": [], + "properties": { + "ConnectorType": { + "type": "string", + "enum": [ + "cType2", + "sType2" + ], + "default": "sType2", + "description": "Used to indicate the type of connector used by the unit", + "readOnly": true + }, + "ConfigLastUpdatedBy": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "LOCAL", + "CPMS" + ] + }, + "description": "Variable used to indicate how the Charge Points configuration was last updated", + "readOnly": true + } + } +})"; + +class SchemaTest : public testing::Test { + static void format_checker(const std::string& format, const std::string& value) { + EVLOG_error << "format_checker: '" << format << "' '" << value << '\''; + } + + static void loader(const json_uri& uri, json& schema) { + schema = nlohmann::json_schema::draft7_schema_builtin; + } + + class custom_error_handler : public basic_error_handler { + private: + void error(const json::json_pointer& pointer, const json& instance, const std::string& message) override { + basic_error_handler::error(pointer, instance, message); + EVLOG_error << "'" << pointer << "' - '" << instance << "': " << message; + errors = true; + } + + public: + bool errors{false}; + constexpr bool has_errors() const { + return errors; + } + }; + +protected: + std::unique_ptr validator; + json schema; + custom_error_handler err; + + void SetUp() override { + schema = json::parse(test_schema); + validator = std::make_unique(&loader, &format_checker); + validator->set_root_schema(schema); + err.errors = false; + } +}; + +TEST_F(SchemaTest, ValidationText) { + json model = R"({"ConnectorType":"cType2"})"_json; + validator->validate(model, err); + EXPECT_FALSE(err.has_errors()); +} + +TEST_F(SchemaTest, ValidationObj) { + json model; + model["ConnectorType"] = "cType2"; + validator->validate(model, err); + EXPECT_FALSE(err.has_errors()); +} + +TEST_F(SchemaTest, ValidationObjErr) { + json model; + model["ConnectorType"] = "cType3"; + validator->validate(model, err); + EXPECT_TRUE(err.has_errors()); +} + +TEST(SchemaObj, Success) { + ocpp::Schemas schema(std::move(json::parse(test_schema))); + auto validator = schema.get_validator(); + json model; + model["ConnectorType"] = "cType2"; + EXPECT_NO_THROW(validator->validate(model)); +} + +TEST(SchemaObj, Fail) { + ocpp::Schemas schema(std::move(json::parse(test_schema))); + auto validator = schema.get_validator(); + json model; + model["ConnectorType"] = "cType3"; + EXPECT_ANY_THROW(validator->validate(model)); +} + +} // namespace diff --git a/tests/lib/ocpp/v201/mocks/device_model_storage_mock.hpp b/tests/lib/ocpp/v201/mocks/device_model_storage_interface_mock.hpp similarity index 82% rename from tests/lib/ocpp/v201/mocks/device_model_storage_mock.hpp rename to tests/lib/ocpp/v201/mocks/device_model_storage_interface_mock.hpp index e882a66ad..7589dce78 100644 --- a/tests/lib/ocpp/v201/mocks/device_model_storage_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/device_model_storage_interface_mock.hpp @@ -5,10 +5,10 @@ #include -#include "ocpp/v201/device_model_storage.hpp" +#include "ocpp/v201/device_model_storage_interface.hpp" namespace ocpp::v201 { -class DeviceModelStorageMock : public DeviceModelStorage { +class DeviceModelStorageMock : public DeviceModelStorageInterface { public: MOCK_METHOD(DeviceModelMap, get_device_model, ()); MOCK_METHOD(std::optional, get_variable_attribute, @@ -24,5 +24,7 @@ class DeviceModelStorageMock : public DeviceModelStorage { MOCK_METHOD(ClearMonitoringStatusEnum, clear_variable_monitor, (int, bool)); MOCK_METHOD(int32_t, clear_custom_variable_monitors, ()); MOCK_METHOD(void, check_integrity, ()); + MOCK_METHOD(bool, update_monitoring_reference, (int32_t monitor_id, const std::string& reference_value), + (override)); }; } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/mocks/message_dispatcher_mock.hpp b/tests/lib/ocpp/v201/mocks/message_dispatcher_mock.hpp new file mode 100644 index 000000000..463445d96 --- /dev/null +++ b/tests/lib/ocpp/v201/mocks/message_dispatcher_mock.hpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include "gmock/gmock.h" + +#include + +using namespace ocpp::v201; + +class MockMessageDispatcher : public ocpp::MessageDispatcherInterface { +public: + MOCK_METHOD(void, dispatch_call, (const json& call, bool triggered), (override)); + MOCK_METHOD(std::future>, dispatch_call_async, + (const json& call, bool triggered), (override)); + MOCK_METHOD(void, dispatch_call_result, (const json& call_result), (override)); + MOCK_METHOD(void, dispatch_call_error, (const json& call_error), (override)); +}; diff --git a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp index a1b2e32cb..795ee3269 100644 --- a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp @@ -23,7 +23,8 @@ class SmartChargingHandlerMock : public SmartChargingHandlerInterface { MOCK_METHOD(ClearChargingProfileResponse, clear_profiles, (const ClearChargingProfileRequest& request), (override)); MOCK_METHOD(std::vector, get_reported_profiles, (const GetChargingProfilesRequest& request), (const, override)); - MOCK_METHOD(std::vector, get_valid_profiles, (int32_t evse_id)); + MOCK_METHOD(std::vector, get_valid_profiles, + (int32_t evse_id, const std::set&)); MOCK_METHOD(CompositeSchedule, calculate_composite_schedule, (std::vector & valid_profiles, const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int32_t evse_id, diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 013e28756..dbf68db22 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -540,13 +540,13 @@ TEST_F(ChargePointConstructorTestFixtureV201, CreateChargePoint_InitializeInCorr } TEST_F(ChargePointConstructorTestFixtureV201, - CreateChargePoint_EVSEConnectorStructureDefinedBadly_ThrowsDeviceModelStorageError) { + CreateChargePoint_EVSEConnectorStructureDefinedBadly_ThrowsDeviceModelError) { configure_callbacks_with_mocks(); auto evse_connector_structure = std::map(); EXPECT_THROW(ocpp::v201::ChargePoint(evse_connector_structure, device_model, database_handler, create_message_queue(database_handler), "/tmp", evse_security, callbacks), - DeviceModelStorageError); + DeviceModelError); } TEST_F(ChargePointConstructorTestFixtureV201, CreateChargePoint_MissingDeviceModel_ThrowsInvalidArgument) { @@ -853,7 +853,8 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, DEFAULT_TX_ID), }; - ON_CALL(*smart_charging_handler, get_valid_profiles(DEFAULT_EVSE_ID)).WillByDefault(testing::Return(profiles)); + ON_CALL(*smart_charging_handler, get_valid_profiles(DEFAULT_EVSE_ID, testing::_)) + .WillByDefault(testing::Return(profiles)); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(profiles, testing::_, testing::_, DEFAULT_EVSE_ID, req.chargingRateUnit)); @@ -869,7 +870,7 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, auto get_composite_schedule_req = request_to_enhanced_message(req); - EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_, testing::_)).Times(0); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) .Times(0); @@ -890,7 +891,7 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, device_model->set_value(charging_rate_unit_cv.component, charging_rate_unit_cv.variable.value(), AttributeEnum::Actual, "A", "test", true); - EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_, testing::_)).Times(0); EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) .Times(0); diff --git a/tests/lib/ocpp/v201/test_composite_schedule.cpp b/tests/lib/ocpp/v201/test_composite_schedule.cpp index 877eed408..eb603864d 100644 --- a/tests/lib/ocpp/v201/test_composite_schedule.cpp +++ b/tests/lib/ocpp/v201/test_composite_schedule.cpp @@ -25,7 +25,6 @@ #include #include -#include #include #include #include diff --git a/tests/lib/ocpp/v201/test_device_model_storage_sqlite.cpp b/tests/lib/ocpp/v201/test_device_model_storage_sqlite.cpp index 2197fc600..28a0da2ba 100644 --- a/tests/lib/ocpp/v201/test_device_model_storage_sqlite.cpp +++ b/tests/lib/ocpp/v201/test_device_model_storage_sqlite.cpp @@ -25,7 +25,7 @@ TEST_F(DeviceModelStorageSQLiteTest, test_check_integrity_valid) { TEST_F(DeviceModelStorageSQLiteTest, test_check_integrity_invalid) { auto dm_storage = DeviceModelStorageSqlite(INVALID_DEVICE_MODEL_DATABASE); - EXPECT_THROW(dm_storage.check_integrity(), DeviceModelStorageError); + EXPECT_THROW(dm_storage.check_integrity(), DeviceModelError); } } // namespace v201 diff --git a/tests/lib/ocpp/v201/test_init_device_model_db.cpp b/tests/lib/ocpp/v201/test_init_device_model_db.cpp index cd41175ec..74fbbd0cc 100644 --- a/tests/lib/ocpp/v201/test_init_device_model_db.cpp +++ b/tests/lib/ocpp/v201/test_init_device_model_db.cpp @@ -87,7 +87,8 @@ class InitDeviceModelDbTest : public DatabaseTestingUtils { /// bool variable_exists(const std::string& component_name, const std::optional& component_instance, const std::optional& component_evse_id, const std::optional& component_connector_id, - const std::string& variable_name, const std::optional& variable_instance); + const std::string& variable_name, const std::optional& variable_instance, + const std::optional source = std::nullopt); /// /// \brief Check if variable characteristics exists in the database. @@ -243,7 +244,7 @@ TEST_F(InitDeviceModelDbTest, init_db) { EXPECT_TRUE(characteristics_exists("UnitTestCtrlr", std::nullopt, 2, 3, "UnitTestPropertyAName", std::nullopt, DataEnum::boolean, std::nullopt, std::nullopt, true, std::nullopt, std::nullopt)); - EXPECT_TRUE(variable_exists("UnitTestCtrlr", std::nullopt, 2, 3, "UnitTestPropertyAName", std::nullopt)); + EXPECT_TRUE(variable_exists("UnitTestCtrlr", std::nullopt, 2, 3, "UnitTestPropertyAName", std::nullopt, "OCPP")); EXPECT_TRUE(variable_exists("UnitTestCtrlr", std::nullopt, 2, 3, "UnitTestPropertyBName", std::nullopt)); EXPECT_TRUE(variable_exists("UnitTestCtrlr", std::nullopt, 2, 3, "UnitTestPropertyCName", std::nullopt)); @@ -589,7 +590,8 @@ bool InitDeviceModelDbTest::variable_exists(const std::string& component_name, const std::optional& component_evse_id, const std::optional& component_connector_id, const std::string& variable_name, - const std::optional& variable_instance) { + const std::optional& variable_instance, + const std::optional source) { static const std::string select_variable_statement = "SELECT ID " "FROM VARIABLE v " "WHERE v.COMPONENT_ID=(" @@ -600,7 +602,8 @@ bool InitDeviceModelDbTest::variable_exists(const std::string& component_name, "AND c.EVSE_ID IS @evse_id " "AND c.CONNECTOR_ID IS @connector_id) " "AND v.NAME=@variable_name " - "AND v.INSTANCE IS @variable_instance"; + "AND v.INSTANCE IS @variable_instance " + "AND v.source IS @variable_source"; std::unique_ptr statement = this->database->new_statement(select_variable_statement); @@ -632,6 +635,12 @@ bool InitDeviceModelDbTest::variable_exists(const std::string& component_name, statement->bind_null("@variable_instance"); } + if (source.has_value()) { + statement->bind_text("@variable_source", source.value(), ocpp::common::SQLiteString::Transient); + } else { + statement->bind_null("@variable_source"); + } + if (statement->step() == SQLITE_ERROR) { return false; } diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index 07614af40..0a6334fe6 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -19,7 +19,7 @@ #include #include -#include +#include #include #include #include diff --git a/tests/resources/unittest_device_model.db b/tests/resources/unittest_device_model.db index 5150f0356..f1b3d8b8a 100644 Binary files a/tests/resources/unittest_device_model.db and b/tests/resources/unittest_device_model.db differ diff --git a/tests/resources/unittest_device_model_missing_required.db b/tests/resources/unittest_device_model_missing_required.db index 503769c9e..fc5ea45a8 100644 Binary files a/tests/resources/unittest_device_model_missing_required.db and b/tests/resources/unittest_device_model_missing_required.db differ