diff --git a/config/v16/profile_schemas/CostAndPrice.json b/config/v16/profile_schemas/CostAndPrice.json index d4d1d40c3..f76d9527f 100644 --- a/config/v16/profile_schemas/CostAndPrice.json +++ b/config/v16/profile_schemas/CostAndPrice.json @@ -113,7 +113,7 @@ "type": "boolean", "readOnly": false }, - "MultiLanguageSupportedLanguages": { + "SupportedLanguages": { "description": "Comma separated list of supported language codes, per RFC5646.", "type": "string", "readOnly": true, diff --git a/config/v201/component_config/standardized/CustomizationCtrlr.json b/config/v201/component_config/standardized/CustomizationCtrlr.json index e8cefa606..33b295d45 100644 --- a/config/v201/component_config/standardized/CustomizationCtrlr.json +++ b/config/v201/component_config/standardized/CustomizationCtrlr.json @@ -32,6 +32,38 @@ ], "description": "Custom implementation has been enabled.", "type": "boolean" + }, + "CustomImplementationCaliforniaPricingEnabled": { + "variable_name": "CustomImplementationEnabled", + "instance": "org.openchargealliance.costmsg", + "characteristics": { + "supportsMonitoring": true, + "dataType": "boolean" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Custom implementation org.openchargealliance.costmsg (California Pricing) has been enabled.", + "type": "boolean" + }, + "CustomImplementationMultiLanguageEnabled": { + "variable_name": "CustomImplementationEnabled", + "instance": "org.openchargealliance.multilanguage", + "characteristics": { + "supportsMonitoring": true, + "dataType": "boolean" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Custom implementation org.openchargealliance.multilanguage has been enabled.", + "type": "boolean" } }, "required": [] diff --git a/config/v201/component_config/standardized/DisplayMessageCtrlr.json b/config/v201/component_config/standardized/DisplayMessageCtrlr.json index 3e1e002a7..288db73d4 100644 --- a/config/v201/component_config/standardized/DisplayMessageCtrlr.json +++ b/config/v201/component_config/standardized/DisplayMessageCtrlr.json @@ -98,6 +98,54 @@ ], "description": "List of the priorities supported by this Charging Station.", "type": "string" + }, + "DisplayMessageSupportedStates": { + "variable_name": "SupportedStates", + "characteristics": { + "valuesList": "Charging,Faulted,Idle,Unavailable", + "supportsMonitoring": true, + "dataType": "MemberList" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "description": "List of the priorities supported by this Charging Station.", + "type": "string", + "default": "Charging,Faulted,Idle,Unavailable" + }, + "QRCodeDisplayCapable": { + "variable_name": "QRCodeDisplayCapable", + "characteristics": { + "dataType": "boolean", + "supportsMonitoring": true + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadOnly" + } + ], + "description": "Whether the station can display QR codes or not.", + "type": "boolean" + }, + "DisplayMessageLanguage": { + "variable_name": "Language", + "characteristics": { + "valuesList": "en_US,de,nl", + "supportsMonitoring": true, + "dataType": "OptionList" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Default language of the charging station. Note: set all supported languages by this charging station in 'valuesList' of this Variable.", + "type": "string" } }, "required": [ diff --git a/config/v201/component_config/standardized/TariffCostCtrlr.json b/config/v201/component_config/standardized/TariffCostCtrlr.json index a3f1c3a8f..5b3ee0dad 100644 --- a/config/v201/component_config/standardized/TariffCostCtrlr.json +++ b/config/v201/component_config/standardized/TariffCostCtrlr.json @@ -119,6 +119,101 @@ ], "description": "Message to be shown to an EV Driver when the Charging Station cannot retrieve the cost for a transaction at the end of the transaction.", "type": "string" + }, + "OfflineChargingPricekWhPrice": { + "variable_name": "OfflineChargingPrice", + "instance": "kWhPrice", + "characteristics": { + "supportsMonitoring": true, + "dataType": "decimal" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Charging kWh price in the default currency when charging station is offline.", + "type": "number" + }, + "OfflineChargingPriceHourPrice": { + "variable_name": "OfflineChargingPrice", + "instance": "hourPrice", + "characteristics": { + "supportsMonitoring": true, + "dataType": "decimal" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Charging kWh price in the default currency when charging station is offline.", + "type": "number" + }, + "TariffFallbackMessageEn": { + "variable_name": "TariffFallbackMessage", + "instance": "en-US", + "characteristics": { + "supportsMonitoring": true, + "dataType": "string" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Message (and / or tariff information) to be shown to an EV Driver when there is no driver specific tariff information available. Note: Add a TariffFallbackMessage with correct instance for every supported language!!", + "type": "string" + }, + "OfflineTariffFallbackMessageEn": { + "variable_name": "OfflineTariffFallbackMessage", + "instance": "en", + "characteristics": { + "supportsMonitoring": true, + "dataType": "string" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Message (and/or tariff information) to be shown to an EV Driver when Charging Station is offline. Note: Add a OfflineTariffFallbackMessage with correct instance for every supported language!!", + "type": "string" + }, + "TotalCostFallbackMessageEn": { + "variable_name": "TotalCostFallbackMessage", + "instance": "en-US", + "characteristics": { + "supportsMonitoring": true, + "dataType": "string" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Message to be shown to an EV Driver when the Charging Station cannot retrieve the cost for a transaction at the end of the transaction. Note: Add a TotalCostFallbackMessage with correct instance for every supported language!!", + "type": "string" + }, + "NumberOfDecimalsForCostValues": { + "variable_name": "NumberOfDecimalsForCostValues", + "characteristics": { + "supportsMonitoring": true, + "dataType": "integer" + }, + "attributes": [ + { + "type": "Actual", + "mutability": "ReadWrite" + } + ], + "description": "Number of decimals for the cost values. Value will be ", + "type": "integer" } }, "required": [ diff --git a/doc/california_pricing_requirements.md b/doc/california_pricing_requirements.md new file mode 100644 index 000000000..78ad57fa5 --- /dev/null +++ b/doc/california_pricing_requirements.md @@ -0,0 +1,117 @@ +# California Pricing Requirements + +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. + +## 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 +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 +`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 +`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 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 / +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, +the DataTransfer message is converted to internally used structs as described above. + +### Configuration Items + +| Name | Description | +| ---- | ----------- | +| `CustomDisplayCostAndPrice` | Set to `true` to enable California Pricing (readonly) | +| `DefaultPrice` | Holds the default price and default price text in a json object. Can be updated by the CSMS. Not used by libocpp. See the specification for the specific fields. | +| `NumberOfDecimalsForCostValues` | Holds the number of decimals the cost / price values are converted with. | +| `CustomIdleFeeAfterStop` | Set to `true` to extend the transaction until `ConnectorUnplugged` is sent (readonly). The chargepoint implementation should send this DataTransfer message, this is not part of libocpp (yet, 2024-08) | +| `CustomMultiLanguageMessages` | Set to `true` to enable multi language support (readonly). | +| `Language` | Default language code for the stations UI (IETF RFC5646) (readwrite). | +| `SupportedLanguages` | Comma separated list of supported languages, specified as IETF RFC5646 (readonly). | +| `DefaultPriceText` | Holds an array (`priceTexts`) of default price texts in several languages. Each item has the `priceText` (string) in the given `language` (string) and a `priceTextOffline` (string) in the given language. The CSMS sends the DefaultPriceText per language: `"DefaultPriceText,\"`, but libocpp will convert it to the above described json object. (readwrite) | +| `TimeOffset` | As OCPP 1.6 does not have built-in support for timezones, you can set a timezone when displaying time related pricing information. This timezone is also used for the `atTime` trigger. (readwrite) | +| `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. + +### Device Model Variables + +| Variable name | Instance | Component | Description | +| ------------- | -------- | --------- | ----------- | +| `CustomImplementationEnabled` | `org.openchargealliance.costmsg` | `CustomizationCtrlr` | Set to 'true' to support California Pricing (actually to indicate `customData` fields for California Pricing are supported). | +| `Enabled` | `Tariff` | `TariffCostCtrlr` | Enable showing tariffs. | +| `Enabled` | `Cost` | `TariffCostCtrlr` | Enable showing of cost. | +| `TariffFallbackMessage` | | `TariffCostCtrlr` | Fallback message to show to EV Driver when there is no driver specific tariff information. Not used by libocpp. | +| `TotalCostFallbackMessage` | | `TariffCostCtrlr` | Fallback message to sho to EV Driver when CS can not retrieve the cost for a transaction at the end of the transaction. Not used by libocpp. | +| `Currency` | | `TariffCostCtrlr` | Currency used for tariff and cost information. | +| `NumberOfDecimalsForCostValues` | | `TariffCostCtrlr` | Holds the number of decimals the cost / price values are converted with. | +| `TariffFallbackMessage` | `Offline` | `TariffCostCtrlr` | Fallback message to be shown to an EV Driver when CS is offline. Not used by libocpp. | +| `OfflineChargingPrice` | `kWhPrice` | `TariffCostCtrlr` | The energy (kWh) price for transactions started while offline. Not used by libocpp. | +| `OfflineChargingPrice` | `hourPrice` | `TariffCostCtrlr` | The time (hour) price for transactions started while offline. Not used by libocpp. | +| `QRCodeDisplayCapable` | | `DisplayMessageCtrlr` | Set to 'true' if station can display QR codes | +| `CustomImplementationEnabled` | `org.openchargealliance.multilanguage` | `CustomizationCtrlr` | Enable multilanguage | +| `TariffFallbackMessage` | `` | `TariffCostCtrlr` | TariffFallbackMessage in a specific language. There must be a variable with the language as instance for every supported language. | +| `OfflineTariffFallbackMessage` | `` | `TariffCostCtrlr` | TariffFallbackMessage when charging station is offline, in a specific language. There must be a variable with the language as instance for every supported language. | +| `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 +(`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. + +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. diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index e909586fb..43e4afb7e 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1125,50 +1125,50 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| -| I01.FR.01 | | | -| I01.FR.02 | | | -| I01.FR.03 | | | +| I01.FR.01 | 🌐 | | +| I01.FR.02 | 🌐 | | +| I01.FR.03 | ⛽️ | | ## TariffAndCost - Show EV Driver Running Total Cost During Charging | ID | Status | Remark | |-----------|--------|--------| -| I02.FR.01 | | | -| I02.FR.02 | | | -| I02.FR.03 | | | -| I02.FR.04 | | | +| I02.FR.01 | 🌐 | | +| I02.FR.02 | ✅ | | +| I02.FR.03 | ⛽️ | | +| I02.FR.04 | ⛽️ | | ## TariffAndCost - Show EV Driver Final Total Cost After Charging | ID | Status | Remark | |-----------|--------|--------| -| I03.FR.01 | | | -| I03.FR.02 | | | -| I03.FR.03 | | | -| I03.FR.04 | | | -| I03.FR.05 | | | +| I03.FR.01 | ✅ | | +| I03.FR.02 | 🌐 | | +| I03.FR.03 | ⛽️ | | +| I03.FR.04 | 🌐 | | +| I03.FR.05 | ⛽️ | | ## TariffAndCost - Show Fallback Tariff Information | ID | Status | Remark | |-----------|--------|--------| -| I04.FR.01 | | | -| I04.FR.02 | | | +| I04.FR.01 | ⛽️ | | +| I04.FR.02 | 🌐 | | ## TariffAndCost - Show Fallback Total Cost Message | ID | Status | Remark | |-----------|--------|--------| -| I05.FR.01 | | | -| I05.FR.02 | | | +| I05.FR.01 | 🌐 | | +| I05.FR.02 | ⛽️ | | ## TariffAndCost - Update Tariff Information During Transaction | ID | Status | Remark | |-----------|--------|--------| -| I06.FR.01 | | | -| I06.FR.02 | | | -| I06.FR.03 | | | +| I06.FR.01 | 🌐 | | +| I06.FR.02 | 🌐 | | +| I06.FR.03 | ⛽️ | | ## MeterValues - Sending Meter Values not related to a transaction @@ -1795,80 +1795,81 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| -| O01.FR.01 | | | -| O01.FR.02 | | | -| O01.FR.03 | | | -| O01.FR.04 | | | -| O01.FR.05 | | | -| O01.FR.06 | | | -| O01.FR.07 | | | -| O01.FR.08 | | | -| O01.FR.09 | | | -| O01.FR.10 | | | -| O01.FR.11 | | | -| O01.FR.12 | | | -| O01.FR.13 | | | -| O01.FR.14 | | | -| O01.FR.15 | | | -| O01.FR.16 | | | -| O01.FR.17 | | | +| O01.FR.01 | ✅ | | +| O01.FR.02 | ✅ | | +| O01.FR.03 | ✅ | | +| O01.FR.04 | 🌐 | | +| O01.FR.05 | 🌐 | | +| O01.FR.06 | ⛽️ | | +| O01.FR.07 | ⛽️ | | +| O01.FR.08 | ⛽️ | | +| O01.FR.09 | ⛽️ | | +| O01.FR.10 | ⛽️ | | +| O01.FR.11 | ⛽️ | | +| O01.FR.12 | ⛽️ | | +| O01.FR.13 | ⛽️ | | +| O01.FR.14 | ⛽️ | | +| O01.FR.15 | ⛽️ | | +| O01.FR.16 | ⛽️ | | +| O01.FR.17 | ⛽️ / 🌐 | | ## DisplayMessage - Set DisplayMessage for Transaction | ID | Status | Remark | |-----------|--------|--------| -| O02.FR.01 | | | -| O02.FR.02 | | | -| O02.FR.03 | | | -| O02.FR.04 | | | -| O02.FR.05 | | | -| O02.FR.06 | | | -| O02.FR.07 | | | -| O02.FR.08 | | | -| O02.FR.09 | | | -| O02.FR.10 | | | -| O02.FR.11 | | | -| O02.FR.12 | | | -| O02.FR.14 | | | -| O02.FR.15 | | | -| O02.FR.16 | | | -| O02.FR.17 | | | -| O02.FR.18 | | | +| O02.FR.01 | ✅ | | +| O02.FR.02 | ⛽️ | | +| O02.FR.03 | ✅ | | +| O02.FR.04 | ✅ | | +| O02.FR.05 | ✅ | | +| O02.FR.06 | ⛽️ | | +| O02.FR.07 | ⛽️ | | +| O02.FR.08 | ⛽️ | | +| O02.FR.09 | ⛽️ | | +| O02.FR.10 | ⛽️ | | +| O02.FR.11 | ⛽️ | | +| O02.FR.12 | ⛽️ / 🌐 | | +| O02.FR.14 | ⛽️ | | +| O02.FR.15 | ⛽️ | | +| O02.FR.16 | ⛽️ | | +| O02.FR.17 | ⛽️ | | +| O02.FR.18 | ⛽️ | | ## DisplayMessage - Get All DisplayMessages | ID | Status | Remark | |-----------|--------|--------| -| O03.FR.02 | | | +| O03.FR.01 | ✅ | | +| O03.FR.02 | ✅ | | | O03.FR.03 | | | | O03.FR.04 | | | | O03.FR.05 | | | -| O03.FR.06 | | | +| O03.FR.06 | ✅ | | ## DisplayMessage - Get Specific DisplayMessages | ID | Status | Remark | |-----------|--------|--------| -| O04.FR.01 | | | -| O04.FR.02 | | | -| O04.FR.03 | | | +| O04.FR.01 | ✅ | | +| O04.FR.02 | ✅ | | +| O04.FR.03 | ✅ | | | O04.FR.04 | | | | O04.FR.05 | | | | O04.FR.06 | | | -| O04.FR.07 | | | +| O04.FR.07 | ✅ | | ## DisplayMessage - Clear a DisplayMessage | ID | Status | Remark | |-----------|--------|--------| -| O05.FR.01 | | | -| O05.FR.02 | | | +| O05.FR.01 | ⛽️ | | +| O05.FR.02 | ⛽️ | | ## DisplayMessage - Replace DisplayMessage | ID | Status | Remark | |-----------|--------|--------| -| O06.FR.01 | | | +| O06.FR.01 | ⛽️ | | ## DataTransfer - Data Transfer to the Charging Station diff --git a/include/ocpp/common/types.hpp b/include/ocpp/common/types.hpp index f1f0b1d74..6a4813329 100644 --- a/include/ocpp/common/types.hpp +++ b/include/ocpp/common/types.hpp @@ -353,13 +353,23 @@ struct DisplayMessageContent { friend void to_json(json& j, const DisplayMessageContent& m); }; +/// +/// \brief Type of an identifier string. +/// +enum class IdentifierType { + SessionId, ///< \brief Identifier is the session id. + IdToken, ///< \brief Identifier is the id token. + TransactionId ///< \brief Identifier is the transaction id. +}; + struct DisplayMessage { std::optional id; std::optional priority; std::optional state; std::optional timestamp_from; std::optional timestamp_to; - std::optional transaction_id; + std::optional identifier_id; + std::optional identifier_type; DisplayMessageContent message; std::optional qr_code; }; diff --git a/include/ocpp/common/utils.hpp b/include/ocpp/common/utils.hpp index cbeed531b..8c196a0de 100644 --- a/include/ocpp/common/utils.hpp +++ b/include/ocpp/common/utils.hpp @@ -23,9 +23,10 @@ bool is_boolean(const std::string& value); /// \brief Split string on a given character. /// \param string_to_split The string to split. /// \param c The character to split the string on. +/// \param trim True if all strings must be trimmed as well. Defaults to 'false'. /// \return A vector with the string 'segments'. /// -std::vector split_string(const std::string& string_to_split, const char c); +std::vector split_string(const std::string& string_to_split, const char c, const bool trim = false); /// /// \brief Trim string, removing leading and trailing white spaces. diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 0895db88f..b34095350 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -31,13 +32,16 @@ #include #include #include +#include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -59,6 +63,7 @@ #include #include #include +#include #include #include #include @@ -80,124 +85,6 @@ class UnexpectedMessageTypeFromCSMS : public std::runtime_error { using std::runtime_error::runtime_error; }; -struct 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. - /// - ///\retval false if any of the normal callbacks are nullptr or any of the optional ones are filled with a nullptr - /// true otherwise - bool all_callbacks_valid() const; - - /// - /// \brief Callback if reset is allowed. If evse_id has a value, reset only applies to the given evse id. If it has - /// no value, applies to complete charging station. - /// - std::function evse_id, const ResetEnum& reset_type)> - is_reset_allowed_callback; - std::function evse_id, const ResetEnum& reset_type)> reset_callback; - std::function stop_transaction_callback; - std::function pause_charging_callback; - - /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of the charging station changed - /// If as a result the state of EVSEs or connectors changed as well, libocpp will additionally call the - /// evse_effective_operative_status_changed_callback once for each EVSE whose status changed, and - /// connector_effective_operative_status_changed_callback once for each connector whose status changed. - /// If left empty, the callback is ignored. - /// \param new_status The operational status the CS switched to - std::optional> - cs_effective_operative_status_changed_callback; - - /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of an EVSE changed - /// If as a result the state of connectors changed as well, libocpp will additionally call the - /// connector_effective_operative_status_changed_callback once for each connector whose status changed. - /// If left empty, the callback is ignored. - /// \param evse_id The id of the EVSE - /// \param new_status The operational status the EVSE switched to - std::optional> - evse_effective_operative_status_changed_callback; - - /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of a connector changed. - /// \param evse_id The id of the EVSE - /// \param connector_id The ID of the connector within the EVSE - /// \param new_status The operational status the connector switched to - std::function - connector_effective_operative_status_changed_callback; - - std::function get_log_request_callback; - std::function unlock_connector_callback; - // callback to be called when the request can be accepted. authorize_remote_start indicates if Authorize.req needs - // to follow or not - std::function - remote_start_transaction_callback; - /// - /// \brief Check if the current reservation for the given evse id is made for the id token / group id token. - /// \return True if evse is reserved for the given id token / group id token, false if it is reserved for another - /// one. - /// - std::function idToken, - const std::optional> groupIdToken)> - is_reservation_for_token_callback; - std::function update_firmware_request_callback; - // callback to be called when a variable has been changed by the CSMS - std::optional> variable_changed_callback; - // callback is called when receiving a SetNetworkProfile.req from the CSMS - std::optional> - validate_network_profile_callback; - std::optional> - configure_network_connection_profile_callback; - std::optional> time_sync_callback; - - /// \brief callback to be called to congfigure ocpp message logging - std::optional> ocpp_messages_callback; - - /// - /// \brief callback function that can be used to react to a security event callback. This callback is - /// called only if the SecurityEvent occured internally within libocpp - /// Typically this callback is used to log security events in the security log - /// - std::function& event_type, const std::optional>& tech_info)> - security_event_callback; - - /// \brief Callback for indicating when a charging profile is received and was accepted. - std::function set_charging_profiles_callback; - - /// \brief Callback for when a bootnotification response is received - std::optional> - boot_notification_callback; - - /// \brief Callback function that can be used to get (human readable) customer information based on the given - /// arguments - std::optional customer_certificate, - const std::optional id_token, - const std::optional> customer_identifier)>> - get_customer_information_callback; - - /// \brief Callback function that can be called to clear customer information based on the given arguments - std::optional customer_certificate, - const std::optional id_token, - const std::optional> customer_identifier)>> - clear_customer_information_callback; - - /// \brief Callback function that can be called when all connectors are unavailable - std::optional> all_connectors_unavailable_callback; - - /// \brief Callback function that can be used to handle arbitrary data transfers for all vendorId and - /// messageId - std::optional> data_transfer_callback; - - /// \brief Callback function that is called when a transaction_event was sent to the CSMS - std::optional> transaction_event_callback; - - /// \brief Callback function that is called when a transaction_event_response was received from the CSMS - std::optional> - transaction_event_response_callback; - - /// \brief Callback function is called when the websocket connection status changes - std::optional> connection_state_changed_callback; -}; - /// \brief Combines ChangeAvailabilityRequest with persist flag for scheduled Availability changes struct AvailabilityChange { ChangeAvailabilityRequest request; @@ -660,6 +547,36 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa /// @param message_log_path path to file logging void configure_message_logging_format(const std::string& message_log_path); + /// + /// \brief Create cost and / or tariff message and call the callbacks to send it, if tariff and / or cost is + /// enabled. + /// \param response The TransactionEventResponse where the tariff and cost information is added to. + /// \param original_message The original TransactionEventRequest, which contains some information we need as + /// well. + /// \param original_transaction_event_response The original json from the response. + /// + void handle_cost_and_tariff(const TransactionEventResponse& response, + const TransactionEventRequest& original_message, + const json& original_transaction_event_response); + + /// + /// \brief Check if multilanguage setting (variable) is enabled. + /// \return True if enabled. + /// + bool is_multilanguage_enabled() const; + + /// + /// \brief Check if tariff setting (variable) is enabled. + /// \return True if enabled. + /// + bool is_tariff_enabled() const; + + /// + /// \brief Check if cost setting (variable) is enabled. + /// \return True if enabled. + /// + bool is_cost_enabled() const; + /* OCPP message requests */ // Functional Block A: Security @@ -745,6 +662,9 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_change_availability_req(Call call); void handle_heartbeat_response(CallResult call); + // Functional Block I: TariffAndCost + void handle_costupdated_req(const Call call); + // Functional Block K: Smart Charging void handle_set_charging_profile_req(Call call); void handle_clear_charging_profile_req(Call call); @@ -769,6 +689,11 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_get_monitoring_report_req(Call call); void handle_clear_variable_monitoring_req(Call call); + // Functional Block O: DisplayMessage + void handle_get_display_message(Call call); + void handle_set_display_message(Call call); + void handle_clear_display_message(Call call); + // Functional Block P: DataTransfer void handle_data_transfer_req(Call call); diff --git a/include/ocpp/v201/charge_point_callbacks.hpp b/include/ocpp/v201/charge_point_callbacks.hpp new file mode 100644 index 000000000..dc70dc338 --- /dev/null +++ b/include/ocpp/v201/charge_point_callbacks.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ocpp::v201 { +struct 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. + /// + /// \retval false if any of the normal callbacks are nullptr or any of the optional ones are filled with a nullptr + /// true otherwise + bool all_callbacks_valid(std::shared_ptr device_model) const; + + /// + /// \brief Callback if reset is allowed. If evse_id has a value, reset only applies to the given evse id. If it has + /// no value, applies to complete charging station. + /// + std::function evse_id, const ResetEnum& reset_type)> + is_reset_allowed_callback; + std::function evse_id, const ResetEnum& reset_type)> reset_callback; + std::function stop_transaction_callback; + std::function pause_charging_callback; + + /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of the charging station changed + /// If as a result the state of EVSEs or connectors changed as well, libocpp will additionally call the + /// evse_effective_operative_status_changed_callback once for each EVSE whose status changed, and + /// connector_effective_operative_status_changed_callback once for each connector whose status changed. + /// If left empty, the callback is ignored. + /// \param new_status The operational status the CS switched to + std::optional> + cs_effective_operative_status_changed_callback; + + /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of an EVSE changed + /// If as a result the state of connectors changed as well, libocpp will additionally call the + /// connector_effective_operative_status_changed_callback once for each connector whose status changed. + /// If left empty, the callback is ignored. + /// \param evse_id The id of the EVSE + /// \param new_status The operational status the EVSE switched to + std::optional> + evse_effective_operative_status_changed_callback; + + /// \brief Used to notify the user of libocpp that the Operative/Inoperative state of a connector changed. + /// \param evse_id The id of the EVSE + /// \param connector_id The ID of the connector within the EVSE + /// \param new_status The operational status the connector switched to + std::function + connector_effective_operative_status_changed_callback; + + std::function get_log_request_callback; + std::function unlock_connector_callback; + // callback to be called when the request can be accepted. authorize_remote_start indicates if Authorize.req needs + // to follow or not + std::function + remote_start_transaction_callback; + /// + /// \brief Check if the current reservation for the given evse id is made for the id token / group id token. + /// \return True if evse is reserved for the given id token / group id token, false if it is reserved for another + /// one. + /// + std::function idToken, + const std::optional> groupIdToken)> + is_reservation_for_token_callback; + std::function update_firmware_request_callback; + // callback to be called when a variable has been changed by the CSMS + std::optional> variable_changed_callback; + // callback is called when receiving a SetNetworkProfile.req from the CSMS + std::optional> + validate_network_profile_callback; + std::optional> + configure_network_connection_profile_callback; + std::optional> time_sync_callback; + + /// \brief callback to be called to congfigure ocpp message logging + std::optional> ocpp_messages_callback; + + /// + /// \brief callback function that can be used to react to a security event callback. This callback is + /// called only if the SecurityEvent occured internally within libocpp + /// Typically this callback is used to log security events in the security log + /// + std::function& event_type, const std::optional>& tech_info)> + security_event_callback; + + /// \brief Callback for indicating when a charging profile is received and was accepted. + std::function set_charging_profiles_callback; + + /// \brief Callback for when a bootnotification response is received + std::optional> + boot_notification_callback; + + /// \brief Callback function that can be used to get (human readable) customer information based on the given + /// arguments + std::optional customer_certificate, + const std::optional id_token, + const std::optional> customer_identifier)>> + get_customer_information_callback; + + /// \brief Callback function that can be called to clear customer information based on the given arguments + std::optional customer_certificate, + const std::optional id_token, + const std::optional> customer_identifier)>> + clear_customer_information_callback; + + /// \brief Callback function that can be called when all connectors are unavailable + std::optional> all_connectors_unavailable_callback; + + /// \brief Callback function that can be used to handle arbitrary data transfers for all vendorId and + /// messageId + std::optional> data_transfer_callback; + + /// \brief Callback function that is called when a transaction_event was sent to the CSMS + std::optional> transaction_event_callback; + + /// \brief Callback function that is called when a transaction_event_response was received from the CSMS + std::optional> + transaction_event_response_callback; + + /// \brief Callback function is called when the websocket connection status changes + std::optional> connection_state_changed_callback; + + /// \brief Callback functions called for get / set / clear display messages + std::optional(const GetDisplayMessagesRequest& request)>> + get_display_message_callback; + std::optional& display_messages)>> + set_display_message_callback; + std::optional> + clear_display_message_callback; + + /// \brief Callback function is called when running cost is set. + std::optional currency_code)>> + set_running_cost_callback; +}; +} // namespace ocpp::v201 diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index ce1d73c68..aadfd645a 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -119,6 +119,8 @@ extern const ComponentVariable& TimeOffsetNextTransition; extern const RequiredComponentVariable& TimeSource; extern const ComponentVariable& TimeZone; extern const ComponentVariable& CustomImplementationEnabled; +extern const ComponentVariable& CustomImplementationCaliforniaPricingEnabled; +extern const ComponentVariable& CustomImplementationMultiLanguageEnabled; extern const RequiredComponentVariable& BytesPerMessageGetReport; extern const RequiredComponentVariable& BytesPerMessageGetVariables; extern const RequiredComponentVariable& BytesPerMessageSetVariables; @@ -131,6 +133,9 @@ extern const ComponentVariable& DisplayMessageCtrlrAvailable; extern const RequiredComponentVariable& NumberOfDisplayMessages; extern const RequiredComponentVariable& DisplayMessageSupportedFormats; extern const RequiredComponentVariable& DisplayMessageSupportedPriorities; +extern const ComponentVariable& DisplayMessageSupportedStates; +extern const ComponentVariable& DisplayMessageQRCodeDisplayCapable; +extern const ComponentVariable& DisplayMessageLanguage; extern const ComponentVariable& CentralContractValidationAllowed; extern const RequiredComponentVariable& ContractValidationOffline; extern const ComponentVariable& RequestMeteringReceipt; @@ -211,6 +216,7 @@ extern const ComponentVariable& TariffCostCtrlrEnabledTariff; extern const ComponentVariable& TariffCostCtrlrEnabledCost; extern const RequiredComponentVariable& TariffFallbackMessage; extern const RequiredComponentVariable& TotalCostFallbackMessage; +extern const ComponentVariable& NumberOfDecimalsForCostValues; extern const RequiredComponentVariable& EVConnectionTimeOut; extern const ComponentVariable& MaxEnergyOnInvalidId; extern const RequiredComponentVariable& StopTxOnEVSideDisconnect; diff --git a/include/ocpp/v201/device_model.hpp b/include/ocpp/v201/device_model.hpp index f74eac9f7..51b8f5c5c 100644 --- a/include/ocpp/v201/device_model.hpp +++ b/include/ocpp/v201/device_model.hpp @@ -115,7 +115,7 @@ class DeviceModel { /// \return GetVariableStatusEnum that indicates the result of the request GetVariableStatusEnum request_value_internal(const Component& component_id, const Variable& variable_id, const AttributeEnum& attribute_enum, std::string& value, - bool allow_write_only); + bool allow_write_only) const; /// \brief Iterates over the given \p component_criteria and converts this to the variable names /// (Active,Available,Enabled,Problem). If any of the variables can not be found as part of a component this @@ -149,7 +149,7 @@ class DeviceModel { /// \return the requested value from the device model storage template T get_value(const RequiredComponentVariable& component_variable, - const AttributeEnum& attribute_enum = AttributeEnum::Actual) { + const AttributeEnum& attribute_enum = AttributeEnum::Actual) const { std::string value; auto response = GetVariableStatusEnum::UnknownVariable; if (component_variable.variable.has_value()) { @@ -175,7 +175,7 @@ class DeviceModel { /// requested from the storage and a value is present for this combination, else std::nullopt . template std::optional get_optional_value(const ComponentVariable& component_variable, - const AttributeEnum& attribute_enum = AttributeEnum::Actual) { + const AttributeEnum& attribute_enum = AttributeEnum::Actual) const { std::string value; auto response = GetVariableStatusEnum::UnknownVariable; if (component_variable.variable.has_value()) { diff --git a/include/ocpp/v201/evse.hpp b/include/ocpp/v201/evse.hpp index af68dc8d5..f53cf7d19 100644 --- a/include/ocpp/v201/evse.hpp +++ b/include/ocpp/v201/evse.hpp @@ -124,6 +124,20 @@ class EvseInterface { /// \brief Returns the phase type for the EVSE based on its SupplyPhases. It can be AC, DC, or Unknown. virtual CurrentPhaseType get_current_phase_type() = 0; + + /// + /// \brief Set metervalue triggers for California Pricing. + /// \param trigger_metervalue_on_power_kw Send metervalues on this amount of kw (with hysteresis). + /// \param trigger_metervalue_on_energy_kwh Send metervalues when this kwh is reached. + /// \param trigger_metervalue_at_time Send metervalues at a specific time. + /// \param send_metervalue_function Function used to send the metervalues. + /// \param io_service io service for the timers. + /// + virtual void set_meter_value_pricing_triggers( + std::optional trigger_metervalue_on_power_kw, std::optional trigger_metervalue_on_energy_kwh, + std::optional trigger_metervalue_at_time, + std::function& meter_values)> send_metervalue_function, + boost::asio::io_service& io_service) = 0; }; /// \brief Represents an EVSE. An EVSE can contain multiple Connector objects, but can only supply energy to one of @@ -142,6 +156,13 @@ class Evse : public EvseInterface { Everest::SteadyTimer sampled_meter_values_timer; std::shared_ptr database_handler; + std::optional trigger_metervalue_on_power_kw; + std::optional trigger_metervalue_on_energy_kwh; + std::unique_ptr trigger_metervalue_at_time_timer; + std::optional last_triggered_metervalue_power_kw; + std::function& meter_values)> send_metervalue_function; + boost::asio::io_service io_service; + /// \brief gets the active import energy meter value from meter_value, normalized to Wh. std::optional get_active_import_register_meter_value(); @@ -152,6 +173,19 @@ class Evse : public EvseInterface { /// \param timestamp void start_metering_timers(const DateTime& timestamp); + /// + /// \brief Send metervalue to CSMS after a pricing trigger occured. + /// \param meter_value The metervalue to send. + /// + void send_meter_value_on_pricing_trigger(const MeterValue& meter_value); + + /// + /// \brief Reset pricing triggers. + /// + /// Resets timer, set all pricing trigger related members to std::nullopt and / or nullptr. + /// + void reset_pricing_triggers(void); + AverageMeterValues aligned_data_updated; AverageMeterValues aligned_data_tx_end; @@ -181,6 +215,8 @@ class Evse : public EvseInterface { transaction_meter_value_req, const std::function& pause_charging_callback); + virtual ~Evse(); + int32_t get_id() const; uint32_t get_number_of_connectors() const; @@ -216,6 +252,20 @@ class Evse : public EvseInterface { void restore_connector_operative_status(int32_t connector_id); CurrentPhaseType get_current_phase_type(); + + /// + /// \brief Set pricing triggers to send the meter value. + /// \param trigger_metervalue_on_power_kw Trigger for this amount of kw + /// \param trigger_metervalue_on_energy_kwh Trigger when amount of kwh is reached + /// \param trigger_metervalue_at_time Trigger for a specific time + /// \param send_metervalue_function Function to send metervalues when trigger 'fires' + /// \param io_service Io service needed for the timer + /// + void set_meter_value_pricing_triggers( + std::optional trigger_metervalue_on_power_kw, std::optional trigger_metervalue_on_energy_kwh, + std::optional trigger_metervalue_at_time, + std::function& meter_values)> send_metervalue_function, + boost::asio::io_service& io_service); }; } // namespace v201 diff --git a/include/ocpp/v201/utils.hpp b/include/ocpp/v201/utils.hpp index 3d7e20038..ef23aa24a 100644 --- a/include/ocpp/v201/utils.hpp +++ b/include/ocpp/v201/utils.hpp @@ -41,6 +41,14 @@ std::vector get_meter_values_with_measurands_applied( const std::vector& aligned_tx_ended_measurands, ocpp::DateTime max_timestamp, bool include_sampled_signed = true, bool include_aligned_signed = true); +/// +/// \brief Set reading context of metervalue sampled values. +/// \param meter_value The meter value to set context on +/// \param reading_context Reading context to set. +/// \return The metervalue with the reading context +/// +MeterValue set_meter_value_reading_context(const MeterValue& meter_value, const ReadingContextEnum reading_context); + /// \brief Returns the given \p str hashed using SHA256 /// \param str /// \return diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9060f896c..c59c90513 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -56,6 +56,7 @@ if(LIBOCPP_ENABLE_V201) PRIVATE ocpp/v201/average_meter_values.cpp ocpp/v201/charge_point.cpp + ocpp/v201/charge_point_callbacks.cpp ocpp/v201/smart_charging.cpp ocpp/v201/connector.cpp ocpp/v201/ctrlr_component_variables.cpp diff --git a/lib/ocpp/common/utils.cpp b/lib/ocpp/common/utils.cpp index 3f204cf24..0cdbd02b0 100644 --- a/lib/ocpp/common/utils.cpp +++ b/lib/ocpp/common/utils.cpp @@ -76,12 +76,15 @@ bool is_rfc3339_datetime(const std::string& value) { return std::regex_match(value, datetime_pattern); } -std::vector split_string(const std::string& string_to_split, const char c) { +std::vector split_string(const std::string& string_to_split, const char c, const bool trim) { std::stringstream input(string_to_split); std::string temp; std::vector result; while (std::getline(input, temp, c)) { + if (trim) { + temp = trim_string(temp); + } result.push_back(temp); } diff --git a/lib/ocpp/v16/charge_point_configuration.cpp b/lib/ocpp/v16/charge_point_configuration.cpp index 63f2ead75..6e2357154 100644 --- a/lib/ocpp/v16/charge_point_configuration.cpp +++ b/lib/ocpp/v16/charge_point_configuration.cpp @@ -1323,7 +1323,8 @@ KeyValue ChargePointConfiguration::getNumberOfConnectorsKeyValue() { // Reservation Profile std::optional ChargePointConfiguration::getReserveConnectorZeroSupported() { std::optional reserve_connector_zero_supported = std::nullopt; - if (this->config.contains("Reservation") && this->config["Reservation"].contains("ReserveConnectorZeroSupported")) { + if (this->config.contains("Reservation") and + this->config["Reservation"].contains("ReserveConnectorZeroSupported")) { reserve_connector_zero_supported.emplace(this->config["Reservation"]["ReserveConnectorZeroSupported"]); } return reserve_connector_zero_supported; @@ -1735,14 +1736,14 @@ std::string hexToString(std::string const& s) { } bool isHexNotation(std::string const& s) { - bool is_hex = s.size() > 2 && s.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos; + bool is_hex = s.size() > 2 and s.find_first_not_of("0123456789abcdefABCDEF", 2) == std::string::npos; if (is_hex) { // check if every char is printable for (size_t i = 0; i < s.length(); i += 2) { std::string byte = s.substr(i, 2); char chr = (char)(int)strtol(byte.c_str(), NULL, 16); - if ((chr < 0x20 || chr > 0x7e) && chr != 0xa) { + if ((chr < 0x20 or chr > 0x7e) and chr != 0xa) { return false; } } @@ -1792,15 +1793,15 @@ bool ChargePointConfiguration::isConnectorPhaseRotationValid(std::string str) { } try { auto connector = std::stoi(e.substr(0, 1)); - if (connector < 0 || connector > this->getNumberOfConnectors()) { + if (connector < 0 or connector > this->getNumberOfConnectors()) { return false; } } catch (const std::invalid_argument&) { return false; } std::string phase_rotation = e.substr(2, 5); - if (phase_rotation != "RST" && phase_rotation != "RTS" && phase_rotation != "SRT" && phase_rotation != "STR" && - phase_rotation != "TRS" && phase_rotation != "TSR") { + if (phase_rotation != "RST" and phase_rotation != "RTS" and phase_rotation != "SRT" and + phase_rotation != "STR" and phase_rotation != "TRS" and phase_rotation != "TSR") { return false; } } @@ -1821,13 +1822,13 @@ bool ChargePointConfiguration::checkTimeOffset(const std::string& offset) { const int32_t minutes = std::stoi(times.at(1)); // And check if numbers are valid. - if (hours < -24 || hours > 24) { + if (hours < -24 or hours > 24) { EVLOG_error << "Could not set display time offset: hours should be between -24 and +24, but is " << times.at(0); return false; } - if (minutes < 0 || minutes > 59) { + if (minutes < 0 or minutes > 59) { EVLOG_error << "Could not set display time offset: minutes should be between 0 and 59, but is " << times.at(1); return false; @@ -1844,7 +1845,7 @@ bool ChargePointConfiguration::checkTimeOffset(const std::string& offset) { } bool isBool(const std::string& str) { - return str == "true" || str == "false"; + return str == "true" or str == "false"; } std::optional ChargePointConfiguration::getAuthorizationKeyKeyValue() { @@ -2312,7 +2313,7 @@ KeyValue ChargePointConfiguration::getWaitForStopTransactionsOnResetTimeoutKeyVa // California Pricing Requirements bool ChargePointConfiguration::getCustomDisplayCostAndPriceEnabled() { - if (this->config.contains("CostAndPrice") && + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("CustomDisplayCostAndPrice")) { return this->config["CostAndPrice"]["CustomDisplayCostAndPrice"]; } @@ -2330,7 +2331,7 @@ KeyValue ChargePointConfiguration::getCustomDisplayCostAndPriceEnabledKeyValue() } std::optional ChargePointConfiguration::getPriceNumberOfDecimalsForCostValues() { - if (this->config.contains("CostAndPrice") && + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("NumberOfDecimalsForCostValues")) { return this->config["CostAndPrice"]["NumberOfDecimalsForCostValues"]; } @@ -2352,7 +2353,7 @@ std::optional ChargePointConfiguration::getPriceNumberOfDecimalsForCos } std::optional ChargePointConfiguration::getDefaultPriceText(const std::string& language) { - if (this->config.contains("CostAndPrice") && this->config.at("CostAndPrice").contains("DefaultPriceText")) { + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("DefaultPriceText")) { bool found = false; json result = json::object(); json& default_price = this->config["CostAndPrice"]["DefaultPriceText"]; @@ -2415,7 +2416,7 @@ ConfigurationStatus ChargePointConfiguration::setDefaultPriceText(const CiString } json default_price = json::object(); - if (this->config.contains("CostAndPrice") && this->config.at("CostAndPrice").contains("DefaultPriceText")) { + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("DefaultPriceText")) { json result = json::object(); default_price = this->config["CostAndPrice"]["DefaultPriceText"]; } @@ -2460,7 +2461,7 @@ KeyValue ChargePointConfiguration::getDefaultPriceTextKeyValue(const std::string } std::optional> ChargePointConfiguration::getAllDefaultPriceTextKeyValues() { - if (this->config.contains("CostAndPrice") && this->config.at("CostAndPrice").contains("DefaultPriceText")) { + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("DefaultPriceText")) { std::vector key_values; const json& default_price = this->config["CostAndPrice"]["DefaultPriceText"]; if (!default_price.contains("priceTexts")) { @@ -2493,7 +2494,7 @@ std::optional> ChargePointConfiguration::getAllDefaultPric } std::optional ChargePointConfiguration::getDefaultPrice() { - if (this->config.contains("CostAndPrice") && this->config.at("CostAndPrice").contains("DefaultPrice")) { + if (this->config.contains("CostAndPrice") and this->config.at("CostAndPrice").contains("DefaultPrice")) { return this->config["CostAndPrice"]["DefaultPrice"].dump(2); } @@ -2530,7 +2531,7 @@ std::optional ChargePointConfiguration::getDefaultPriceKeyValue() { } std::optional ChargePointConfiguration::getDisplayTimeOffset() { - if (this->config.contains("CostAndPrice") && this->config["CostAndPrice"].contains("TimeOffset")) { + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("TimeOffset")) { return this->config["CostAndPrice"]["TimeOffset"]; } @@ -2560,7 +2561,7 @@ std::optional ChargePointConfiguration::getDisplayTimeOffsetKeyValue() } std::optional ChargePointConfiguration::getNextTimeOffsetTransitionDateTime() { - if (this->config.contains("CostAndPrice") && + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("NextTimeOffsetTransitionDateTime")) { return this->config["CostAndPrice"]["NextTimeOffsetTransitionDateTime"]; } @@ -2594,7 +2595,7 @@ std::optional ChargePointConfiguration::getNextTimeOffsetTransitionDat } std::optional ChargePointConfiguration::getTimeOffsetNextTransition() { - if (this->config.contains("CostAndPrice") && this->config["CostAndPrice"].contains("TimeOffsetNextTransition")) { + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("TimeOffsetNextTransition")) { return this->config["CostAndPrice"]["TimeOffsetNextTransition"]; } @@ -2624,7 +2625,7 @@ std::optional ChargePointConfiguration::getTimeOffsetNextTransitionKey } std::optional ChargePointConfiguration::getCustomIdleFeeAfterStop() { - if (this->config.contains("CostAndPrice") && this->config["CostAndPrice"].contains("CustomIdleFeeAfterStop")) { + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("CustomIdleFeeAfterStop")) { return this->config["CostAndPrice"]["CustomIdleFeeAfterStop"]; } @@ -2650,7 +2651,8 @@ std::optional ChargePointConfiguration::getCustomIdleFeeAfterStopKeyVa } std::optional ChargePointConfiguration::getCustomMultiLanguageMessagesEnabled() { - if (this->config.contains("CostAndPrice") && this->config["CostAndPrice"].contains("CustomMultiLanguageMessages")) { + if (this->config.contains("CostAndPrice") and + this->config["CostAndPrice"].contains("CustomMultiLanguageMessages")) { return this->config["CostAndPrice"]["CustomMultiLanguageMessages"]; } @@ -2671,9 +2673,8 @@ std::optional ChargePointConfiguration::getCustomMultiLanguageMessages } std::optional ChargePointConfiguration::getMultiLanguageSupportedLanguages() { - if (this->config.contains("CostAndPrice") && - this->config["CostAndPrice"].contains("MultiLanguageSupportedLanguages")) { - return this->config["CostAndPrice"]["MultiLanguageSupportedLanguages"]; + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("SupportedLanguages")) { + return this->config["CostAndPrice"]["SupportedLanguages"]; } return std::nullopt; @@ -2684,7 +2685,7 @@ std::optional ChargePointConfiguration::getMultiLanguageSupportedLangu std::optional languages = getMultiLanguageSupportedLanguages(); if (languages.has_value()) { result = KeyValue(); - result->key = "MultiLanguageSupportedLanguages"; + result->key = "SupportedLanguages"; result->value = languages.value(); result->readonly = true; } @@ -2693,7 +2694,7 @@ std::optional ChargePointConfiguration::getMultiLanguageSupportedLangu } std::optional ChargePointConfiguration::getLanguage() { - if (this->config.contains("CostAndPrice") && this->config["CostAndPrice"].contains("Language")) { + if (this->config.contains("CostAndPrice") and this->config["CostAndPrice"].contains("Language")) { return this->config["CostAndPrice"]["Language"]; } @@ -3072,7 +3073,7 @@ std::optional ChargePointConfiguration::get(CiString<50> key) { if (key == "DefaultPrice") { return this->getDefaultPriceKeyValue(); } - if (key.get().find("DefaultPriceText") == 0 && this->getCustomMultiLanguageMessagesEnabled().has_value() && + if (key.get().find("DefaultPriceText") == 0 and this->getCustomMultiLanguageMessagesEnabled().has_value() and this->getCustomMultiLanguageMessagesEnabled().value()) { const std::vector message_language = split_string(key, ','); if (message_language.size() > 1) { @@ -3091,7 +3092,7 @@ std::optional ChargePointConfiguration::get(CiString<50> key) { if (key == "CustomIdleFeeAfterStop") { return this->getCustomIdleFeeAfterStopKeyValue(); } - if (key == "MultiLanguageSupportedLanguages") { + if (key == "SupportedLanguages") { return this->getMultiLanguageSupportedLanguagesKeyValue(); } if (key == "CustomMultiLanguageMessages") { diff --git a/lib/ocpp/v16/charge_point_impl.cpp b/lib/ocpp/v16/charge_point_impl.cpp index 7f530410b..49006e9e6 100644 --- a/lib/ocpp/v16/charge_point_impl.cpp +++ b/lib/ocpp/v16/charge_point_impl.cpp @@ -3002,14 +3002,19 @@ DataTransferResponse ChargePointImpl::handle_set_user_price(const std::optional< std::vector messages; DisplayMessage message; const auto t = this->transaction_handler->get_transaction_from_id_tag(id_token.value()); + std::string identifier_id; + IdentifierType identifier_type; if (t == nullptr) { - EVLOG_error << "Set user price failed: could not get session id from transaction."; - return response; + identifier_id = id_token.value(); + identifier_type = IdentifierType::IdToken; + } else { + identifier_id = t->get_session_id(); + identifier_type = IdentifierType::SessionId; } - std::string session_id = t->get_session_id(); - message.transaction_id = session_id; + message.identifier_id = identifier_id; + message.identifier_type = identifier_type; if (data.contains("priceText")) { message.message.message = data.at("priceText"); @@ -3024,7 +3029,8 @@ DataTransferResponse ChargePointImpl::handle_set_user_price(const std::optional< data.at("priceTextExtra").is_array()) { for (const json& j : data.at("priceTextExtra")) { DisplayMessage display_message; - display_message.transaction_id = session_id; + display_message.identifier_id = identifier_id; + display_message.identifier_type = identifier_type; display_message.message = j; messages.push_back(display_message); diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 3467b92f3..c9920692d 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,7 @@ 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; @@ -29,37 +31,9 @@ const auto DEFAULT_BOOT_NOTIFICATION_RETRY_INTERVAL = std::chrono::seconds(30); const auto DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD = 2E5; const auto DEFAULT_MAX_MESSAGE_SIZE = 65000; -bool Callbacks::all_callbacks_valid() const { - return this->is_reset_allowed_callback != nullptr and this->reset_callback != nullptr and - this->stop_transaction_callback != nullptr and this->pause_charging_callback != nullptr and - this->connector_effective_operative_status_changed_callback != nullptr and - this->get_log_request_callback != nullptr and this->unlock_connector_callback != nullptr and - this->remote_start_transaction_callback != nullptr and this->is_reservation_for_token_callback != nullptr and - this->update_firmware_request_callback != nullptr and this->security_event_callback != nullptr and - this->set_charging_profiles_callback != nullptr and - (!this->variable_changed_callback.has_value() or this->variable_changed_callback.value() != nullptr) and - (!this->validate_network_profile_callback.has_value() or - this->validate_network_profile_callback.value() != nullptr) and - (!this->configure_network_connection_profile_callback.has_value() or - this->configure_network_connection_profile_callback.value() != nullptr) and - (!this->time_sync_callback.has_value() or this->time_sync_callback.value() != nullptr) and - (!this->boot_notification_callback.has_value() or this->boot_notification_callback.value() != nullptr) and - (!this->ocpp_messages_callback.has_value() or this->ocpp_messages_callback.value() != nullptr) and - (!this->cs_effective_operative_status_changed_callback.has_value() or - this->cs_effective_operative_status_changed_callback.value() != nullptr) and - (!this->evse_effective_operative_status_changed_callback.has_value() or - this->evse_effective_operative_status_changed_callback.value() != nullptr) and - (!this->get_customer_information_callback.has_value() or - this->get_customer_information_callback.value() != nullptr) and - (!this->clear_customer_information_callback.has_value() or - this->clear_customer_information_callback.value() != nullptr) and - (!this->all_connectors_unavailable_callback.has_value() or - this->all_connectors_unavailable_callback.value() != nullptr) and - (!this->data_transfer_callback.has_value() or this->data_transfer_callback.value() != nullptr) and - (!this->transaction_event_callback.has_value() or this->transaction_event_callback.value() != nullptr) and - (!this->transaction_event_response_callback.has_value() or - this->transaction_event_response_callback.value() != nullptr); -} +static DisplayMessageContent message_content_to_display_message_content(const MessageContent& message_content); +static std::optional display_message_to_message_info_type(const DisplayMessage& display_message); +static DisplayMessage message_info_to_display_message(const MessageInfo& message_info); ChargePoint::ChargePoint(const std::map& evse_connector_structure, std::shared_ptr device_model, std::shared_ptr database_handler, @@ -87,15 +61,15 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct v2g_certificate_expiration_check_timer([this]() { this->scheduled_check_v2g_certificate_expiration(); }), callbacks(callbacks) { - // Make sure the received callback struct is completely filled early before we actually start running - if (!this->callbacks.all_callbacks_valid()) { - EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); - } - if (!this->device_model) { EVLOG_AND_THROW(std::invalid_argument("Device model should not be null")); } + // Make sure the received callback struct is completely filled early before we actually start running + if (!this->callbacks.all_callbacks_valid(this->device_model)) { + EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); + } + if (!this->database_handler) { EVLOG_AND_THROW(std::invalid_argument("Database handler should not be null")); } @@ -149,7 +123,7 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct callbacks(callbacks), stop_auth_cache_cleanup_handler(false) { // Make sure the received callback struct is completely filled early before we actually start running - if (!this->callbacks.all_callbacks_valid()) { + if (!this->callbacks.all_callbacks_valid(this->device_model)) { EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); } @@ -294,7 +268,7 @@ void ChargePoint::on_firmware_update_status_notification(int32_t request_id, CiString<50>(ocpp::security_events::INVALIDFIRMWARESIGNATURE), std::optional>("Signature of the provided firmware is not valid!"), true, true); // L01.FR.03 - critical because TC_L_06_CS requires this message to be sent - } else if (req.status == FirmwareStatusEnum::InstallVerificationFailed || + } else if (req.status == FirmwareStatusEnum::InstallVerificationFailed or req.status == FirmwareStatusEnum::InstallationFailed) { this->restore_all_connector_states(); } @@ -631,6 +605,169 @@ void ChargePoint::configure_message_logging_format(const std::string& message_lo } } +void ChargePoint::handle_cost_and_tariff(const TransactionEventResponse& response, + const TransactionEventRequest& original_message, + const json& original_transaction_event_response) { + const bool tariff_enabled = this->is_tariff_enabled(); + + const bool cost_enabled = this->is_cost_enabled(); + + std::vector cost_messages; + + // Check if there is a tariff message and if 'Tariff' is available and enabled + if (response.updatedPersonalMessage.has_value() and tariff_enabled) { + MessageContent personal_message = response.updatedPersonalMessage.value(); + DisplayMessageContent message = message_content_to_display_message_content(personal_message); + cost_messages.push_back(message); + + // If cost is enabled, the message will be sent to the running cost callback. But if it is not enabled, the + // tariff message will be sent using the display message callback. + if (!cost_enabled and this->callbacks.set_display_message_callback.has_value() and + this->callbacks.set_display_message_callback != nullptr) { + DisplayMessage display_message; + display_message.message = message; + display_message.identifier_id = original_message.transactionInfo.transactionId; + display_message.identifier_type = IdentifierType::TransactionId; + this->callbacks.set_display_message_callback.value()({display_message}); + } + } + + // Check if cost is available and enabled, and if there is a totalcost message. + if (cost_enabled and response.totalCost.has_value() and this->callbacks.set_running_cost_callback.has_value()) { + RunningCost running_cost; + std::string total_cost; + // We use the original string and convert it to a double ourselves, as the nlohmann library converts it to a + // float first and then multiply by 10^5 for example (5 decimals) will give some rounding errors. With a initial + // double instead of float, we have (a bit) more accuracy. + if (original_transaction_event_response.contains("totalCost")) { + total_cost = original_transaction_event_response.at("totalCost").dump(); + running_cost.cost = stod(total_cost); + } else { + running_cost.cost = static_cast(response.totalCost.value()); + } + + if (original_message.eventType == TransactionEventEnum::Ended) { + running_cost.state = RunningCostState::Finished; + } else { + running_cost.state = RunningCostState::Charging; + } + + running_cost.transaction_id = original_message.transactionInfo.transactionId; + + if (original_message.meterValue.has_value()) { + const auto& meter_value = original_message.meterValue.value(); + std::optional max_meter_value; + for (const MeterValue& mv : meter_value) { + auto it = std::find_if(mv.sampledValue.begin(), mv.sampledValue.end(), [](const SampledValue& value) { + return value.measurand == MeasurandEnum::Energy_Active_Import_Register and !value.phase.has_value(); + }); + if (it != mv.sampledValue.end()) { + // Found a sampled metervalue we are searching for! + if (!max_meter_value.has_value() or max_meter_value.value() < it->value) { + max_meter_value = it->value; + } + } + } + if (max_meter_value.has_value()) { + running_cost.meter_value = static_cast(max_meter_value.value()); + } + } + + running_cost.timestamp = original_message.timestamp; + + if (response.customData.has_value()) { + // With the current spec, it is not possible to send a qr code as well as a multi language personal + // message, because there can only be one vendor id in custom data. If you not check the vendor id, it + // is just possible for a csms to include them both. + const json& custom_data = response.customData.value(); + if (/*custom_data.contains("vendorId") and + (custom_data.at("vendorId").get() == "org.openchargealliance.org.qrcode") and */ + custom_data.contains("qrCodeText") and + device_model->get_optional_value(ControllerComponentVariables::DisplayMessageQRCodeDisplayCapable) + .value_or(false)) { + running_cost.qr_code_text = custom_data.at("qrCodeText"); + } + + // Add multilanguage messages + if (custom_data.contains("updatedPersonalMessageExtra") and is_multilanguage_enabled()) { + // Get supported languages, which is stored in the values list of "Language" of + // "DisplayMessageCtrlr" + std::optional metadata = device_model->get_variable_meta_data( + ControllerComponentVariables::DisplayMessageLanguage.component, + ControllerComponentVariables::DisplayMessageLanguage.variable.value()); + + std::vector supported_languages; + + if (metadata.has_value() and metadata.value().characteristics.valuesList.has_value()) { + supported_languages = + ocpp::split_string(metadata.value().characteristics.valuesList.value(), ',', true); + } else { + EVLOG_error << "DisplayMessageCtrlr variable Language should have a valuesList with supported " + "languages"; + } + + for (const auto& m : custom_data.at("updatedPersonalMessageExtra").items()) { + DisplayMessageContent c = message_content_to_display_message_content(m.value()); + if (!c.language.has_value()) { + EVLOG_warning + << "updated personal message extra sent but language unknown: Can not show message."; + continue; + } + + if (supported_languages.empty()) { + EVLOG_warning << "Can not show personal message as the supported languages are unknown " + "(please set the `valuesList` of `DisplayMessageCtrlr` variable `Language` to " + "set the supported languages)"; + // Break loop because the next iteration, the supported languages will also not be there. + break; + } + + if (std::find(supported_languages.begin(), supported_languages.end(), c.language.value()) != + supported_languages.end()) { + cost_messages.push_back(c); + } else { + EVLOG_warning << "Can not send a personal message text in language " << c.language.value() + << " as it is not supported by the charging station."; + } + } + } + } + + if (tariff_enabled and !cost_messages.empty()) { + running_cost.cost_messages = cost_messages; + } + + const int number_of_decimals = + this->device_model->get_optional_value(ControllerComponentVariables::NumberOfDecimalsForCostValues) + .value_or(DEFAULT_PRICE_NUMBER_OF_DECIMALS); + uint32_t decimals = + (number_of_decimals < 0 ? DEFAULT_PRICE_NUMBER_OF_DECIMALS : static_cast(number_of_decimals)); + const std::optional currency = + this->device_model->get_value(ControllerComponentVariables::TariffCostCtrlrCurrency); + this->callbacks.set_running_cost_callback.value()(running_cost, decimals, currency); + } +} + +bool ChargePoint::is_multilanguage_enabled() const { + return this->device_model + ->get_optional_value(ControllerComponentVariables::CustomImplementationMultiLanguageEnabled) + .value_or(false); +} + +bool ChargePoint::is_tariff_enabled() const { + return this->device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrAvailableTariff) + .value_or(false) and + this->device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrEnabledTariff) + .value_or(false); +} + +bool ChargePoint::is_cost_enabled() const { + return this->device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrAvailableCost) + .value_or(false) and + this->device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrEnabledCost) + .value_or(false); +} + void ChargePoint::on_unavailable(const int32_t evse_id, const int32_t connector_id) { this->evse_manager->get_evse(evse_id).submit_event(connector_id, ConnectorEvent::Unavailable); } @@ -862,12 +999,12 @@ AuthorizeResponse ChargePoint::validate_token(const IdToken id_token, const std: const auto lifetime = this->device_model->get_optional_value(ControllerComponentVariables::AuthCacheLifeTime); const bool lifetime_expired = - lifetime.has_value() && ((cache_entry->last_used.to_time_point() + - std::chrono::seconds(lifetime.value())) < now.to_time_point()); + lifetime.has_value() and ((cache_entry->last_used.to_time_point() + + std::chrono::seconds(lifetime.value())) < now.to_time_point()); const bool cache_expiry_passed = id_token_info.cacheExpiryDateTime.has_value() and (id_token_info.cacheExpiryDateTime.value() < now); - if (lifetime_expired || cache_expiry_passed) { + if (lifetime_expired or cache_expiry_passed) { EVLOG_info << "Found valid entry in AuthCache but " << (lifetime_expired ? "lifetime expired" : "expiry date passed") << ": Removing from cache and sending new request"; @@ -976,7 +1113,7 @@ void ChargePoint::initialize(const std::map& evse_connector_st evse_connector_structure, database_handler, [this](auto evse_id, auto connector_id, auto status, bool initiated_by_trigger_message) { this->update_dm_availability_state(evse_id, connector_id, status); - if (this->connectivity_manager == nullptr || !this->connectivity_manager->is_websocket_connected() || + if (this->connectivity_manager == nullptr or !this->connectivity_manager->is_websocket_connected() or this->registration_status != RegistrationStatusEnum::Accepted) { return false; } else { @@ -1231,6 +1368,18 @@ void ChargePoint::handle_message(const EnhancedMessage& messa case MessageType::ClearVariableMonitoring: this->handle_clear_variable_monitoring_req(json_message); break; + case MessageType::GetDisplayMessages: + this->handle_get_display_message(json_message); + break; + case MessageType::SetDisplayMessage: + this->handle_set_display_message(json_message); + break; + case MessageType::ClearDisplayMessage: + this->handle_clear_display_message(json_message); + break; + case MessageType::CostUpdated: + this->handle_costupdated_req(json_message); + break; default: if (message.messageTypeId == MessageTypeId::CALL) { const auto call_error = CallError(message.uniqueId, "NotImplemented", "", json({})); @@ -1333,7 +1482,7 @@ void ChargePoint::message_callback(const std::string& message) { this->send(call_error); } catch (json::exception& e) { EVLOG_error << "JSON exception during handling of message: " << e.what(); - if (json_message.is_array() && json_message.size() > MESSAGE_ID) { + 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); } @@ -1367,7 +1516,7 @@ void ChargePoint::change_all_connectors_to_unavailable_for_firmware_update() { } } // Check succeeded, trigger the callback if needed - if (this->callbacks.all_connectors_unavailable_callback.has_value() && + if (this->callbacks.all_connectors_unavailable_callback.has_value() and this->are_all_connectors_effectively_inoperative()) { this->callbacks.all_connectors_unavailable_callback.value()(); } @@ -1586,7 +1735,7 @@ void ChargePoint::handle_scheduled_change_availability_requests(const int32_t ev this->execute_change_availability_request(req, persist); this->scheduled_change_availability_requests.erase(evse_id); // Check succeeded, trigger the callback if needed - if (this->callbacks.all_connectors_unavailable_callback.has_value() && + if (this->callbacks.all_connectors_unavailable_callback.has_value() and this->are_all_connectors_effectively_inoperative()) { this->callbacks.all_connectors_unavailable_callback.value()(); } @@ -1606,10 +1755,10 @@ void ChargePoint::handle_scheduled_change_availability_requests(const int32_t ev static bool component_variable_change_requires_websocket_option_update_without_reconnect( const ComponentVariable& component_variable) { - return component_variable == ControllerComponentVariables::RetryBackOffRandomRange || - component_variable == ControllerComponentVariables::RetryBackOffRepeatTimes || - component_variable == ControllerComponentVariables::RetryBackOffWaitMinimum || - component_variable == ControllerComponentVariables::NetworkProfileConnectionAttempts || + return component_variable == ControllerComponentVariables::RetryBackOffRandomRange or + component_variable == ControllerComponentVariables::RetryBackOffRepeatTimes or + component_variable == ControllerComponentVariables::RetryBackOffWaitMinimum or + component_variable == ControllerComponentVariables::NetworkProfileConnectionAttempts or component_variable == ControllerComponentVariables::WebSocketPingInterval; } @@ -1808,7 +1957,7 @@ bool ChargePoint::is_evse_connector_available(EvseInterface& evse) const { evse.get_connector(static_cast(i))->get_effective_connector_status(); // At least one of the connectors is available / not faulted. - if (status != ConnectorStatusEnum::Faulted && status != ConnectorStatusEnum::Unavailable) { + if (status != ConnectorStatusEnum::Faulted and status != ConnectorStatusEnum::Unavailable) { return true; } } @@ -1905,7 +2054,7 @@ void ChargePoint::sign_certificate_req(const ocpp::CertificateSigningUseEnum& ce const auto result = this->evse_security->generate_certificate_signing_request( certificate_signing_use, country.value(), organization.value(), common.value(), should_use_tpm); - if (result.status != GetCertificateSignRequestStatus::Accepted || !result.csr.has_value()) { + if (result.status != GetCertificateSignRequestStatus::Accepted or !result.csr.has_value()) { EVLOG_error << "CSR generation was unsuccessful for sign request: " << ocpp::conversions::certificate_signing_use_enum_to_string(certificate_signing_use); @@ -2059,13 +2208,13 @@ void ChargePoint::transaction_event_req(const TransactionEventEnum& event_type, auto it = std::find_if( remote_start_id_per_evse.begin(), remote_start_id_per_evse.end(), [&id_token, &evse](const std::pair>& remote_start_per_evse) { - if (id_token.has_value() && remote_start_per_evse.second.first.idToken == id_token.value().idToken) { + if (id_token.has_value() and remote_start_per_evse.second.first.idToken == id_token.value().idToken) { if (remote_start_per_evse.first == 0) { return true; } - if (evse.has_value() && evse.value().id == remote_start_per_evse.first) { + if (evse.has_value() and evse.value().id == remote_start_per_evse.first) { return true; } } @@ -2129,7 +2278,7 @@ void ChargePoint::notify_event_req(const std::vector& events) { void ChargePoint::notify_customer_information_req(const std::string& data, const int32_t request_id) { size_t pos = 0; int32_t seq_no = 0; - while (pos < data.length() or pos == 0 && data.empty()) { + while (pos < data.length() or pos == 0 and data.empty()) { const auto req = [&]() { NotifyCustomerInformationRequest req; req.data = CiString<512>(data.substr(pos, 512)); @@ -2268,7 +2417,7 @@ void ChargePoint::handle_boot_notification_response(CallResultregistration_status == RegistrationStatusEnum::Accepted) { this->message_queue->set_registration_status_accepted(); // B01.FR.06 Only use boot timestamp if TimeSource contains Heartbeat - if (this->callbacks.time_sync_callback.has_value() && + if (this->callbacks.time_sync_callback.has_value() and this->device_model->get_value(ControllerComponentVariables::TimeSource).find("Heartbeat") != std::string::npos) { this->callbacks.time_sync_callback.value()(msg.currentTime); @@ -2518,7 +2667,7 @@ void ChargePoint::handle_reset_req(Call call) { bool transaction_active = false; std::set evse_active_transactions; std::set evse_no_transactions; - if (msg.evseId.has_value() && this->evse_manager->get_evse(msg.evseId.value()).has_active_transaction()) { + if (msg.evseId.has_value() and this->evse_manager->get_evse(msg.evseId.value()).has_active_transaction()) { transaction_active = true; evse_active_transactions.emplace(msg.evseId.value()); } else { @@ -2556,7 +2705,7 @@ void ChargePoint::handle_reset_req(Call call) { response.status = ResetStatusEnum::Rejected; } - if (response.status == ResetStatusEnum::Accepted && transaction_active && msg.type == ResetEnum::OnIdle) { + if (response.status == ResetStatusEnum::Accepted and transaction_active and msg.type == ResetEnum::OnIdle) { if (msg.evseId.has_value()) { // B12.FR.07 this->reset_scheduled_evseids.insert(msg.evseId.value()); @@ -2573,13 +2722,13 @@ void ChargePoint::handle_reset_req(Call call) { // Reset response is sent, now set evse connectors to unavailable and / or // stop transaction (depending on reset type) - if (response.status != ResetStatusEnum::Rejected && transaction_active) { + if (response.status != ResetStatusEnum::Rejected and transaction_active) { if (msg.type == ResetEnum::Immediate) { // B12.FR.08 and B12.FR.04 for (const int32_t evse_id : evse_active_transactions) { callbacks.stop_transaction_callback(evse_id, ReasonEnum::ImmediateReset); } - } else if (msg.type == ResetEnum::OnIdle && !evse_no_transactions.empty()) { + } else if (msg.type == ResetEnum::OnIdle and !evse_no_transactions.empty()) { for (const int32_t evse_id : evse_no_transactions) { auto& evse = this->evse_manager->get_evse(evse_id); this->set_evse_connectors_unavailable(evse, false); @@ -2623,6 +2772,8 @@ void ChargePoint::handle_transaction_event_response(const EnhancedMessagecallbacks.transaction_event_response_callback.value()(original_msg, call_result.msg); } + this->handle_cost_and_tariff(call_result.msg, original_msg, message.message[CALLRESULT_PAYLOAD]); + if (original_msg.eventType == TransactionEventEnum::Ended) { // nothing to do for TransactionEventEnum::Ended return; @@ -2958,7 +3109,7 @@ void ChargePoint::handle_remote_start_transaction_request(Call } void ChargePoint::handle_heartbeat_response(CallResult call) { - if (this->callbacks.time_sync_callback.has_value() && + if (this->callbacks.time_sync_callback.has_value() and this->device_model->get_value(ControllerComponentVariables::TimeSource).find("Heartbeat") != std::string::npos) { // the received currentTime was the time the CSMS received the heartbeat request @@ -3126,6 +3277,79 @@ void ChargePoint::handle_heartbeat_response(CallResult call) } } +void ChargePoint::handle_costupdated_req(const Call call) { + CostUpdatedResponse response; + ocpp::CallResult call_result(response, call.uniqueId); + + if (!is_cost_enabled() or !this->callbacks.set_running_cost_callback.has_value()) { + this->send(call_result); + return; + } + + RunningCost running_cost; + TriggerMeterValue triggers; + + if (device_model + ->get_optional_value(ControllerComponentVariables::CustomImplementationCaliforniaPricingEnabled) + .value_or(false) and + call.msg.customData.has_value()) { + const json running_cost_json = call.msg.customData.value(); + + // California pricing is enabled, which means we have to read the custom data. + running_cost = running_cost_json; + + if (running_cost_json.contains("triggerMeterValue")) { + triggers = running_cost_json.at("triggerMeterValue"); + } + } else { + running_cost.state = RunningCostState::Charging; + } + + // In 2.0.1, the cost and transaction id are already part of the CostUpdatedRequest, so they need to be added to + // the 'RunningCost' struct. + running_cost.cost = static_cast(call.msg.totalCost); + running_cost.transaction_id = call.msg.transactionId; + + std::optional transaction_evse_id = get_transaction_evseid(running_cost.transaction_id); + if (!transaction_evse_id.has_value()) { + // We just put an error in the log as the spec does not define what to do here. It is not possible to return + // a 'Rejected' or something in that manner. + EVLOG_error << "Received CostUpdatedRequest, but transaction id is not a valid transaction id."; + } + + const int number_of_decimals = + this->device_model->get_optional_value(ControllerComponentVariables::NumberOfDecimalsForCostValues) + .value_or(DEFAULT_PRICE_NUMBER_OF_DECIMALS); + uint32_t decimals = + (number_of_decimals < 0 ? DEFAULT_PRICE_NUMBER_OF_DECIMALS : static_cast(number_of_decimals)); + const std::optional currency = + this->device_model->get_value(ControllerComponentVariables::TariffCostCtrlrCurrency); + this->callbacks.set_running_cost_callback.value()(running_cost, decimals, currency); + + this->send(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()) { + return; + } + + const std::optional evse_id_opt = get_transaction_evseid(running_cost.transaction_id); + if (!evse_id_opt.has_value()) { + EVLOG_warning << "Can not set running cost triggers as there is no evse id found with the transaction id from " + "the incoming CostUpdatedRequest"; + return; + } + + const int32_t evse_id = evse_id_opt.value(); + auto& evse = this->evse_manager->get_evse(evse_id); + evse.set_meter_value_pricing_triggers( + triggers.at_power_kw, triggers.at_energy_kwh, triggers.at_time, + [this, evse_id](const std::vector& meter_values) { + this->meter_values_req(evse_id, meter_values, false); + }, + this->io_service); +} + void ChargePoint::handle_set_charging_profile_req(Call call) { EVLOG_debug << "Received SetChargingProfileRequest: " << call.msg << "\nwith messageId: " << call.uniqueId; auto msg = call.msg; @@ -3296,7 +3520,7 @@ void ChargePoint::handle_firmware_update_req(Call call) { ocpp::CallResult call_result(response, call.uniqueId); this->send(call_result); - if ((response.status == UpdateFirmwareStatusEnum::InvalidCertificate) || + if ((response.status == UpdateFirmwareStatusEnum::InvalidCertificate) or (response.status == UpdateFirmwareStatusEnum::RevokedCertificate)) { // L01.FR.02 this->security_event_notification_req( @@ -3439,7 +3663,7 @@ void ChargePoint::handle_set_monitoring_base_req(Call } else { response.status = GenericDeviceModelStatusEnum::Accepted; - if (msg.monitoringBase == MonitoringBaseEnum::HardWiredOnly || + if (msg.monitoringBase == MonitoringBaseEnum::HardWiredOnly or msg.monitoringBase == MonitoringBaseEnum::FactoryDefault) { try { this->device_model->clear_custom_monitors(); @@ -3458,7 +3682,7 @@ void ChargePoint::handle_set_monitoring_level_req(Call MonitoringLevelSeverity::MAX) { + if (msg.severity < MonitoringLevelSeverity::MIN or msg.severity > MonitoringLevelSeverity::MAX) { response.status = GenericStatusEnum::Rejected; } else { auto result = this->device_model->set_value( @@ -3543,7 +3767,7 @@ void ChargePoint::notify_monitoring_report_req(const int request_id, // Construct sub-message part std::vector sub_data; - for (int32_t j = i; j < MAXIMUM_VARIABLE_SEND && j < montoring_data.size(); ++j) { + for (int32_t j = i; j < MAXIMUM_VARIABLE_SEND and j < montoring_data.size(); ++j) { sub_data.push_back(std::move(montoring_data[i + j])); } @@ -3611,6 +3835,143 @@ void ChargePoint::handle_clear_variable_monitoring_req(Callsend(call_result); } +void ChargePoint::handle_get_display_message(const Call call) { + GetDisplayMessagesResponse response; + if (!this->callbacks.get_display_message_callback.has_value()) { + response.status = GetDisplayMessagesStatusEnum::Unknown; + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); + return; + } + + // Call 'get display message callback' to get all display messages from the charging station. + const std::vector display_messages = this->callbacks.get_display_message_callback.value()(call.msg); + + NotifyDisplayMessagesRequest messages_request; + messages_request.requestId = call.msg.requestId; + messages_request.messageInfo = std::vector(); + // Convert all display messages from the charging station to the correct format. They will not be included if + // they do not have the required values. That's why we wait with sending the response until we converted all + // display messages, because we then know if there are any. + for (const auto& display_message : display_messages) { + const std::optional message_info = display_message_to_message_info_type(display_message); + if (message_info.has_value()) { + messages_request.messageInfo->push_back(message_info.value()); + } + } + + // Send 'accepted' back to the CSMS if there is at least one message and send all the messages in another + // request. + if (messages_request.messageInfo.value().empty()) { + response.status = GetDisplayMessagesStatusEnum::Unknown; + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); + return; + } else { + response.status = GetDisplayMessagesStatusEnum::Accepted; + ocpp::CallResult call_result(response, call.uniqueId); + this->send(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); +} + +void ChargePoint::handle_set_display_message(const Call call) { + SetDisplayMessageResponse response; + if (!this->callbacks.set_display_message_callback.has_value()) { + response.status = DisplayMessageStatusEnum::Rejected; + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); + return; + } + + // Check if display messages are available, priority and message format are supported and if the given + // transaction is running, if a transaction id was included in the message. + bool error = false; + const std::optional display_message_available = + this->device_model->get_optional_value(ControllerComponentVariables::DisplayMessageCtrlrAvailable); + const std::string supported_priorities = + this->device_model->get_value(ControllerComponentVariables::DisplayMessageSupportedPriorities); + const std::string supported_message_formats = + this->device_model->get_value(ControllerComponentVariables::DisplayMessageSupportedFormats); + + const std::vector priorities = split_string(supported_priorities, ',', true); + const std::vector formats = split_string(supported_message_formats, ',', true); + const auto& supported_priority_it = std::find( + priorities.begin(), priorities.end(), conversions::message_priority_enum_to_string(call.msg.message.priority)); + const auto& supported_format_it = std::find( + formats.begin(), formats.end(), conversions::message_format_enum_to_string(call.msg.message.message.format)); + + // Check if transaction is valid: this is the case if there is no transaction id, or if the transaction id + // belongs to a running transaction. + const bool transaction_valid = (!call.msg.message.transactionId.has_value() or + get_transaction_evseid(call.msg.message.transactionId.value()) != std::nullopt); + + // Check if display messages are available. + if (!display_message_available.has_value() or !display_message_available.value()) { + error = true; + response.status = DisplayMessageStatusEnum::Rejected; + } + // Check if the priority is supported. + else if (supported_priority_it == priorities.end()) { + error = true; + response.status = DisplayMessageStatusEnum::NotSupportedPriority; + } + // Check if the message format is supported. + else if (supported_format_it == formats.end()) { + error = true; + response.status = DisplayMessageStatusEnum::NotSupportedMessageFormat; + } + // Check if transaction is valid. + else if (!transaction_valid) { + error = true; + response.status = DisplayMessageStatusEnum::UnknownTransaction; + } + // Check if message state is supported. + else if (call.msg.message.state.has_value()) { + const std::optional supported_states = this->device_model->get_optional_value( + ControllerComponentVariables::DisplayMessageSupportedStates); + if (supported_states.has_value()) { + const std::vector states = split_string(supported_states.value(), ',', true); + const auto& supported_states_it = + std::find(states.begin(), states.end(), + conversions::message_state_enum_to_string(call.msg.message.state.value())); + if (supported_states_it == states.end()) { + error = true; + response.status = DisplayMessageStatusEnum::NotSupportedState; + } + } + } + + if (error) { + ocpp::CallResult call_result(response, call.uniqueId); + this->send(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); +} + +void ChargePoint::handle_clear_display_message(const Call call) { + ClearDisplayMessageResponse response; + if (!this->callbacks.clear_display_message_callback.has_value()) { + EVLOG_error << "Received a clear display message request, but callback is not implemented."; + response.status = ClearMessageStatusEnum::Unknown; + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); + } + + response = this->callbacks.clear_display_message_callback.value()(call.msg); + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); +} + void ChargePoint::handle_data_transfer_req(Call call) { const auto msg = call.msg; DataTransferResponse response; @@ -3909,7 +4270,7 @@ void ChargePoint::cache_cleanup_handler() { // Wait for next wakeup or timeout std::unique_lock lk(this->auth_cache_cleanup_mutex); if (this->auth_cache_cleanup_cv.wait_for(lk, std::chrono::minutes(15), [&]() { - return this->stop_auth_cache_cleanup_handler || this->auth_cache_cleanup_required; + return this->stop_auth_cache_cleanup_handler or this->auth_cache_cleanup_required; })) { EVLOG_debug << "Triggered authorization cache cleanup"; } else { @@ -3959,7 +4320,7 @@ GetCompositeScheduleResponse ChargePoint::get_composite_schedule_internal(const request.chargingRateUnit.value())) != supported_charging_rate_units.npos; // K01.FR.05 & K01.FR.07 - if (this->evse_manager->does_evse_exist(request.evseId) && unit_supported) { + if (this->evse_manager->does_evse_exist(request.evseId) and unit_supported) { auto start_time = ocpp::DateTime(); auto end_time = ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(request.duration)); @@ -4147,5 +4508,76 @@ std::vector ChargePoint::get_all_composite_schedules(const in return composite_schedules; } +// Static functions + +/// +/// \brief Convert message content from OCPP spec to DisplayMessageContent. +/// \param message_content The struct to convert. +/// \return The converted struct. +/// +static DisplayMessageContent message_content_to_display_message_content(const MessageContent& message_content) { + DisplayMessageContent result; + result.message = message_content.content; + result.message_format = message_content.format; + result.language = message_content.language; + return result; +} + +/// +/// \brief Convert display message to MessageInfo from OCPP. +/// \param display_message The struct to convert. +/// \return The converted struct. +/// +static std::optional display_message_to_message_info_type(const DisplayMessage& display_message) { + // Each display message should have an id and p[riority, this is required for OCPP. + if (!display_message.id.has_value()) { + EVLOG_error << "Can not convert DisplayMessage to MessageInfo: No id is provided, which is required by OCPP."; + return std::nullopt; + } + + if (!display_message.priority.has_value()) { + EVLOG_error + << "Can not convert DisplayMessage to MessageInfo: No priority is provided, which is required by OCPP."; + return std::nullopt; + } + + MessageInfo info; + info.message.content = display_message.message.message; + info.message.format = + (display_message.message.message_format.has_value() ? display_message.message.message_format.value() + : MessageFormatEnum::UTF8); + info.message.language = display_message.message.language; + info.endDateTime = display_message.timestamp_to; + info.startDateTime = display_message.timestamp_from; + info.id = display_message.id.value(); + info.priority = display_message.priority.value(); + info.state = display_message.state; + info.transactionId = display_message.identifier_id; + + // Note: component is (not yet?) supported for display messages in libocpp. + + return info; +} + +/// +/// \brief Convert message info from OCPP to DisplayMessage. +/// \param message_info The struct to convert. +/// \return The converted struct. +/// +static DisplayMessage message_info_to_display_message(const MessageInfo& message_info) { + DisplayMessage display_message; + + display_message.id = message_info.id; + display_message.priority = message_info.priority; + display_message.state = message_info.state; + display_message.timestamp_from = message_info.startDateTime; + display_message.timestamp_to = message_info.endDateTime; + display_message.identifier_id = message_info.transactionId; + display_message.identifier_type = IdentifierType::TransactionId; + display_message.message = message_content_to_display_message_content(message_info.message); + + return display_message; +} + } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/charge_point_callbacks.cpp b/lib/ocpp/v201/charge_point_callbacks.cpp new file mode 100644 index 000000000..93063c6ab --- /dev/null +++ b/lib/ocpp/v201/charge_point_callbacks.cpp @@ -0,0 +1,82 @@ +#include + +#include + +namespace ocpp::v201 { + +bool Callbacks::all_callbacks_valid(std::shared_ptr device_model) const { + bool valid = + this->is_reset_allowed_callback != nullptr and this->reset_callback != nullptr and + this->stop_transaction_callback != nullptr and this->pause_charging_callback != nullptr and + this->connector_effective_operative_status_changed_callback != nullptr and + this->get_log_request_callback != nullptr and this->unlock_connector_callback != nullptr and + this->remote_start_transaction_callback != nullptr and this->is_reservation_for_token_callback != nullptr and + this->update_firmware_request_callback != nullptr and this->security_event_callback != nullptr and + this->set_charging_profiles_callback != nullptr and + (!this->variable_changed_callback.has_value() or this->variable_changed_callback.value() != nullptr) and + (!this->validate_network_profile_callback.has_value() or + this->validate_network_profile_callback.value() != nullptr) and + (!this->configure_network_connection_profile_callback.has_value() or + this->configure_network_connection_profile_callback.value() != nullptr) and + (!this->time_sync_callback.has_value() or this->time_sync_callback.value() != nullptr) and + (!this->boot_notification_callback.has_value() or this->boot_notification_callback.value() != nullptr) and + (!this->ocpp_messages_callback.has_value() or this->ocpp_messages_callback.value() != nullptr) and + (!this->cs_effective_operative_status_changed_callback.has_value() or + this->cs_effective_operative_status_changed_callback.value() != nullptr) and + (!this->evse_effective_operative_status_changed_callback.has_value() or + this->evse_effective_operative_status_changed_callback.value() != nullptr) and + (!this->get_customer_information_callback.has_value() or + this->get_customer_information_callback.value() != nullptr) and + (!this->clear_customer_information_callback.has_value() or + this->clear_customer_information_callback.value() != nullptr) and + (!this->all_connectors_unavailable_callback.has_value() or + this->all_connectors_unavailable_callback.value() != nullptr) and + (!this->data_transfer_callback.has_value() or this->data_transfer_callback.value() != nullptr) and + (!this->transaction_event_callback.has_value() or this->transaction_event_callback.value() != nullptr) and + (!this->transaction_event_response_callback.has_value() or + this->transaction_event_response_callback.value() != nullptr); + + if (valid) { + if (device_model->get_optional_value(ControllerComponentVariables::DisplayMessageCtrlrAvailable) + .value_or(false)) { + if ((!this->clear_display_message_callback.has_value() or + this->clear_display_message_callback.value() == nullptr) or + (!this->get_display_message_callback.has_value() or + this->get_display_message_callback.value() == nullptr) or + (!this->set_display_message_callback.has_value() or + this->set_display_message_callback.value() == nullptr)) { + EVLOG_error << "Display message controller is set to 'Available' in device model, but callbacks are " + "not (all) implemented"; + valid = false; + } + } + + // If cost is available and enabled, the running cost callback must be enabled as well. + if (device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrAvailableCost) + .value_or(false) and + device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrEnabledCost) + .value_or(false)) { + if (!this->set_running_cost_callback.has_value() or this->set_running_cost_callback.value() == nullptr) { + EVLOG_error << "TariffAndCost controller 'Cost' is set to 'Available' and 'Enabled' in device model, " + "but callback is not implemented"; + valid = false; + } + } + + if (device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrAvailableTariff) + .value_or(false) and + device_model->get_optional_value(ControllerComponentVariables::TariffCostCtrlrEnabledTariff) + .value_or(false)) { + if (!this->set_display_message_callback.has_value() or + this->set_display_message_callback.value() == nullptr) { + EVLOG_error + << "TariffAndCost controller 'Tariff' is set to 'Available' and 'Enabled'. In this case, the " + "set_display_message_callback must be implemented to send the tariff, but it is not"; + valid = false; + } + } + } + + return valid; +} +} // namespace ocpp::v201 diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index 3616e30d2..5f4627ea2 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -586,6 +586,16 @@ const ComponentVariable& CustomImplementationEnabled = { "CustomImplementationEnabled", }), }; +const ComponentVariable& CustomImplementationCaliforniaPricingEnabled = { + ControllerComponents::CustomizationCtrlr, + std::nullopt, + std::optional({"CustomImplementationEnabled", std::nullopt, "org.openchargealliance.costmsg"}), +}; +const ComponentVariable& CustomImplementationMultiLanguageEnabled = { + ControllerComponents::CustomizationCtrlr, + std::nullopt, + std::optional({"CustomImplementationEnabled", std::nullopt, "org.openchargealliance.multilanguage"}), +}; const RequiredComponentVariable& BytesPerMessageGetReport = { ControllerComponents::DeviceDataCtrlr, std::nullopt, @@ -658,6 +668,17 @@ const RequiredComponentVariable& DisplayMessageSupportedPriorities = { "SupportedPriorities", }), }; +const ComponentVariable& DisplayMessageSupportedStates = { + ControllerComponents::DisplayMessageCtrlr, std::nullopt, + std::optional({"SupportedStates", std::nullopt, std::nullopt})}; + +const ComponentVariable& DisplayMessageQRCodeDisplayCapable = { + ControllerComponents::DisplayMessageCtrlr, std::nullopt, + std::optional({"QRCodeDisplayCapable", std::nullopt, std::nullopt})}; + +const ComponentVariable& DisplayMessageLanguage = {ControllerComponents::DisplayMessageCtrlr, std::nullopt, + std::optional({"Language", std::nullopt, std::nullopt})}; + const ComponentVariable& CentralContractValidationAllowed = { ControllerComponents::ISO15118Ctrlr, std::nullopt, @@ -1190,6 +1211,11 @@ const RequiredComponentVariable& TotalCostFallbackMessage = { "TotalCostFallbackMessage", }), }; + +const ComponentVariable& NumberOfDecimalsForCostValues = { + ControllerComponents::TariffCostCtrlr, std::nullopt, + std::optional({"NumberOfDecimalsForCostValues", std::nullopt, std::nullopt})}; + const RequiredComponentVariable& EVConnectionTimeOut = { ControllerComponents::TxCtrlr, std::nullopt, diff --git a/lib/ocpp/v201/device_model.cpp b/lib/ocpp/v201/device_model.cpp index c9edde94c..28f119458 100644 --- a/lib/ocpp/v201/device_model.cpp +++ b/lib/ocpp/v201/device_model.cpp @@ -218,7 +218,7 @@ 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) { + bool allow_write_only) const { const auto component_it = this->device_model.find(component_id); if (component_it == this->device_model.end()) { EVLOG_debug << "unknown component in " << component_id.name << "." << variable_id.name; diff --git a/lib/ocpp/v201/evse.cpp b/lib/ocpp/v201/evse.cpp index b43ad66bd..6260131dd 100644 --- a/lib/ocpp/v201/evse.cpp +++ b/lib/ocpp/v201/evse.cpp @@ -71,6 +71,13 @@ Evse::Evse(const int32_t evse_id, const int32_t number_of_connectors, DeviceMode } } +Evse::~Evse() { + if (this->trigger_metervalue_at_time_timer != nullptr) { + this->trigger_metervalue_at_time_timer->stop(); + this->trigger_metervalue_at_time_timer = nullptr; + } +} + int32_t Evse::get_id() const { return this->evse_id; } @@ -218,6 +225,8 @@ void Evse::release_transaction() { EVLOG_error << "Could not clear transaction meter values: " << e.what(); } this->transaction = nullptr; + + this->reset_pricing_triggers(); } std::unique_ptr& Evse::get_transaction() { @@ -234,6 +243,7 @@ void Evse::on_meter_value(const MeterValue& meter_value) { this->aligned_data_updated.set_values(meter_value); this->aligned_data_tx_end.set_values(meter_value); this->check_max_energy_on_invalid_id(); + this->send_meter_value_on_pricing_trigger(meter_value); } MeterValue Evse::get_meter_value() { @@ -380,16 +390,158 @@ void Evse::start_metering_timers(const DateTime& timestamp) { store_aligned_metervalue, aligned_data_tx_ended_interval, std::chrono::floor(date::utc_clock::to_sys(date::utc_clock::now()))); - // Store an extra aligned metervalue to fix the edge case where a transaction is started just before an interval - // but this code is processed just after the interval. - // For example, aligned interval = 1 min, transaction started at 11:59:59.500 and we get here on 12:00:00.100. - // There is still the expectation for us to add a metervalue at timepoint 12:00:00.000 which we do with this. + // Store an extra aligned metervalue to fix the edge case where a transaction is started just before an + // interval but this code is processed just after the interval. For example, aligned interval = 1 min, + // transaction started at 11:59:59.500 and we get here on 12:00:00.100. There is still the expectation for + // us to add a metervalue at timepoint 12:00:00.000 which we do with this. if (date::utc_clock::to_sys(timestamp.to_time_point()) <= (next_interval - aligned_data_tx_ended_interval)) { store_aligned_metervalue(); } } } +void Evse::set_meter_value_pricing_triggers( + std::optional trigger_metervalue_on_power_kw, std::optional trigger_metervalue_on_energy_kwh, + std::optional trigger_metervalue_at_time, + std::function& meter_values)> send_metervalue_function, + boost::asio::io_service& io_service) { + + EVLOG_debug << "Set metervalue pricing triggers: " + << (trigger_metervalue_at_time.has_value() + ? "at time: " + trigger_metervalue_at_time.value().to_rfc3339() + : "no time pricing trigger") + << (trigger_metervalue_on_energy_kwh.has_value() + ? ", on energy kWh: " + std::to_string(trigger_metervalue_on_energy_kwh.value()) + : ", No energy kWh trigger, ") + << (trigger_metervalue_on_power_kw.has_value() + ? ", on power kW: " + std::to_string(trigger_metervalue_on_power_kw.value()) + : ", No power kW trigger"); + + this->send_metervalue_function = send_metervalue_function; + this->trigger_metervalue_on_power_kw = trigger_metervalue_on_power_kw; + this->trigger_metervalue_on_energy_kwh = trigger_metervalue_on_energy_kwh; + if (this->trigger_metervalue_at_time_timer != nullptr and trigger_metervalue_at_time.has_value()) { + this->trigger_metervalue_at_time_timer->stop(); + this->trigger_metervalue_at_time_timer = nullptr; + } + + std::chrono::time_point trigger_timepoint = trigger_metervalue_at_time.value().to_time_point(); + const std::chrono::time_point now = date::utc_clock::now(); + + if (trigger_timepoint < now) { + EVLOG_error << "Could not set trigger metervalue because trigger time is in the past."; + return; + } + + // Start a timer for the trigger 'atTime'. + this->trigger_metervalue_at_time_timer = std::make_unique(&io_service, [this]() { + EVLOG_error << "Sending metervalue in timer"; + + const MeterValue meter_value = utils::get_meter_value_with_measurands_applied( + this->get_meter_value(), {MeasurandEnum::Energy_Active_Import_Register}); + if (meter_value.sampledValue.empty()) { + EVLOG_error << "Send latest meter value because of chargepoint time trigger failed"; + } else { + const MeterValue mv = utils::set_meter_value_reading_context(meter_value, ReadingContextEnum::Other); + this->send_metervalue_function({mv}); + } + }); + EVLOG_error << "Set trigger metervalue at time " << trigger_timepoint; + + this->trigger_metervalue_at_time_timer->at(trigger_timepoint); +} + +void Evse::reset_pricing_triggers() { + this->last_triggered_metervalue_power_kw = std::nullopt; + this->trigger_metervalue_on_power_kw = std::nullopt; + this->trigger_metervalue_on_energy_kwh = std::nullopt; + this->send_metervalue_function = nullptr; + + if (this->trigger_metervalue_at_time_timer != nullptr) { + this->trigger_metervalue_at_time_timer->stop(); + this->trigger_metervalue_at_time_timer = nullptr; + } +} + +void Evse::send_meter_value_on_pricing_trigger(const MeterValue& meter_value) { + bool meter_value_sent = false; + // Check if there is a kwh trigger and if the value is exceeded. + if (this->trigger_metervalue_on_energy_kwh.has_value()) { + const double trigger_energy_kwh = this->trigger_metervalue_on_energy_kwh.value(); + if (this->send_metervalue_function == nullptr) { + EVLOG_error << "Cost and price metervalue kwh trigger: Can not send metervalue because the send metervalue " + "function is not set."; + this->trigger_metervalue_on_energy_kwh.reset(); + } else { + const std::optional active_import_register_meter_value_wh = get_active_import_register_meter_value(); + if (active_import_register_meter_value_wh.has_value() and + static_cast(active_import_register_meter_value_wh.value()) >= trigger_energy_kwh * 1000) { + const MeterValue active_import_meter_value = utils::get_meter_value_with_measurands_applied( + meter_value, {MeasurandEnum::Energy_Active_Import_Register, MeasurandEnum::Power_Active_Import}); + if (active_import_meter_value.sampledValue.empty()) { + EVLOG_error + << "No current active import register metervalue found. Can not send trigger metervalue."; + } else { + const MeterValue to_send = + utils::set_meter_value_reading_context(active_import_meter_value, ReadingContextEnum::Other); + this->send_metervalue_function({to_send}); + this->trigger_metervalue_on_energy_kwh.reset(); + meter_value_sent = true; + } + } + } + } + + // Check if there is a power kw trigger and if that is triggered. For the power kw trigger, we added hysterisis to + // prevent constant triggering. + const std::optional active_power_meter_value = utils::get_total_power_active_import(meter_value); + + if (!this->trigger_metervalue_on_power_kw.has_value() or !active_power_meter_value.has_value()) { + return; + } + + const double trigger_power_kw = this->trigger_metervalue_on_power_kw.value(); + if (this->send_metervalue_function == nullptr) { + EVLOG_error << "Cost and price metervalue wh trigger: Can not send metervalue because the send metervalue " + "function is not set."; + // Remove trigger because next time function is not set as well (this is probably a bug because it should be + // set in the `set_meter_value_pricing_triggers` function together with the trigger values). + this->trigger_metervalue_on_energy_kwh.reset(); + return; + } + + if (this->last_triggered_metervalue_power_kw.has_value()) { + // Hysteresis of 5% to avoid repetitive triggers when the power fluctuates around the trigger level. + const double hysterisis_kw = trigger_power_kw * 0.05; + const double triggered_power_kw = this->last_triggered_metervalue_power_kw.value(); + const double current_metervalue_w = static_cast(active_power_meter_value.value()); + const double current_metervalue_kw = current_metervalue_w / 1000; + + if ( // Check if trigger value is crossed in upward direction. + (triggered_power_kw < trigger_power_kw and current_metervalue_kw >= (trigger_power_kw + hysterisis_kw)) or + // Check if trigger value is crossed in downward direction. + (triggered_power_kw > trigger_power_kw and current_metervalue_kw <= (trigger_power_kw - hysterisis_kw))) { + + // Power threshold is crossed, send metervalues. + if (!meter_value_sent) { + // Only send metervalue if it is not sent yet, otherwise only the last triggered metervalue is set. + const MeterValue mv = utils::set_meter_value_reading_context(meter_value, ReadingContextEnum::Other); + this->send_metervalue_function({mv}); + } + + // Also when metervalue is sent, we want to set the last triggered metervalue. + this->last_triggered_metervalue_power_kw = current_metervalue_kw; + } + } else { + // Send metervalue anyway since we have no previous metervalue stored and don't know if we should send any + if (!meter_value_sent) { + // Only send metervalue if it is not sent yet, otherwise only the last triggered metervalue is set. + this->send_metervalue_function({meter_value}); + } + this->last_triggered_metervalue_power_kw = active_power_meter_value.value() / 1000; + } +} + void Evse::set_evse_operative_status(OperationalStatusEnum new_status, bool persist) { this->component_state_manager->set_evse_individual_operational_status(this->evse_id, new_status, persist); } @@ -407,7 +559,7 @@ OperationalStatusEnum Evse::get_effective_operational_status() { } Connector* Evse::get_connector(int32_t connector_id) { - if (connector_id <= 0 || connector_id > this->get_number_of_connectors()) { + if (connector_id <= 0 or connector_id > this->get_number_of_connectors()) { std::stringstream err_msg; err_msg << "ConnectorID " << connector_id << " out of bounds for EVSE " << this->evse_id; throw std::logic_error(err_msg.str()); @@ -421,7 +573,7 @@ CurrentPhaseType Evse::get_current_phase_type() { auto supply_phases = this->device_model.get_optional_value(evse_variable); if (supply_phases == std::nullopt) { return CurrentPhaseType::Unknown; - } else if (*supply_phases == 1 || *supply_phases == 3) { + } else if (*supply_phases == 1 or *supply_phases == 3) { return CurrentPhaseType::AC; } else if (*supply_phases == 0) { return CurrentPhaseType::DC; diff --git a/lib/ocpp/v201/utils.cpp b/lib/ocpp/v201/utils.cpp index 5d8256c2c..914cb1b6a 100644 --- a/lib/ocpp/v201/utils.cpp +++ b/lib/ocpp/v201/utils.cpp @@ -102,6 +102,15 @@ std::vector get_meter_values_with_measurands_applied( return meter_values_result; } +MeterValue set_meter_value_reading_context(const MeterValue& meter_value, const ReadingContextEnum reading_context) { + MeterValue return_value = meter_value; + for (auto& sampled_value : return_value.sampledValue) { + sampled_value.context = reading_context; + } + + return return_value; +} + std::string sha256(const std::string& str) { unsigned char hash[SHA256_DIGEST_LENGTH]; EVP_Digest(str.c_str(), str.size(), hash, NULL, EVP_sha256(), NULL); diff --git a/tests/lib/ocpp/common/utils_tests.cpp b/tests/lib/ocpp/common/utils_tests.cpp index 09410196d..057d4b9ab 100644 --- a/tests/lib/ocpp/common/utils_tests.cpp +++ b/tests/lib/ocpp/common/utils_tests.cpp @@ -64,6 +64,20 @@ TEST(Utils, test_split_string) { ASSERT_EQ(result.size(), 2); EXPECT_EQ(result.at(0), "This is a test"); EXPECT_EQ(result.at(1), " It is performed using google test"); + + result = split_string("Aa, Bb, Cc, Dd", ',', false); + ASSERT_EQ(result.size(), 4); + EXPECT_EQ(result.at(0), "Aa"); + EXPECT_EQ(result.at(1), " Bb"); + EXPECT_EQ(result.at(2), " Cc"); + EXPECT_EQ(result.at(3), " Dd"); + + result = split_string("Aa, Bb, Cc, Dd", ',', true); + ASSERT_EQ(result.size(), 4); + EXPECT_EQ(result.at(0), "Aa"); + EXPECT_EQ(result.at(1), "Bb"); + EXPECT_EQ(result.at(2), "Cc"); + EXPECT_EQ(result.at(3), "Dd"); } TEST(Utils, test_trim_string) { diff --git a/tests/lib/ocpp/v201/mocks/evse_mock.hpp b/tests/lib/ocpp/v201/mocks/evse_mock.hpp index 4e7b0b50c..da05da0ac 100644 --- a/tests/lib/ocpp/v201/mocks/evse_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/evse_mock.hpp @@ -36,5 +36,11 @@ class EvseMock : public EvseInterface { (int32_t connector_id, OperationalStatusEnum new_status, bool persist)); MOCK_METHOD(void, restore_connector_operative_status, (int32_t connector_id)); MOCK_METHOD(CurrentPhaseType, get_current_phase_type, ()); + MOCK_METHOD(void, set_meter_value_pricing_triggers, + (std::optional trigger_metervalue_on_power_kw, + std::optional trigger_metervalue_on_energy_kwh, + std::optional trigger_metervalue_at_time, + std::function& meter_values)> send_metervalue_function, + boost::asio::io_service& io_service)); }; } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index f1b72ad6e..4d3f2098c 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -387,7 +387,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfSetChargingP configure_callbacks_with_mocks(); callbacks.set_charging_profiles_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } /* @@ -396,40 +396,40 @@ TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfSetChargingP */ TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksAreInvalidWhenNotProvided) { - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksAreValidWhenAllRequiredCallbacksProvided) { configure_callbacks_with_mocks(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfResetIsAllowedCallbackExists) { configure_callbacks_with_mocks(); callbacks.is_reset_allowed_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfResetCallbackExists) { configure_callbacks_with_mocks(); callbacks.reset_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfStopTransactionCallbackExists) { configure_callbacks_with_mocks(); callbacks.stop_transaction_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfPauseChargingCallbackExists) { configure_callbacks_with_mocks(); callbacks.pause_charging_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -437,60 +437,60 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.connector_effective_operative_status_changed_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfGetLogRequestCallbackExists) { configure_callbacks_with_mocks(); callbacks.get_log_request_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfUnlockConnectorCallbackExists) { configure_callbacks_with_mocks(); callbacks.unlock_connector_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfRemoteStartTransactionCallbackExists) { configure_callbacks_with_mocks(); callbacks.remote_start_transaction_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfIsReservationForTokenCallbackExists) { configure_callbacks_with_mocks(); callbacks.is_reservation_for_token_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfUpdateFirmwareRequestCallbackExists) { configure_callbacks_with_mocks(); callbacks.update_firmware_request_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfSecurityEventCallbackExists) { configure_callbacks_with_mocks(); callbacks.security_event_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalVariableChangedCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.variable_changed_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction variable_changed_callback_mock; callbacks.variable_changed_callback = variable_changed_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -498,13 +498,13 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.validate_network_profile_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction validate_network_profile_callback_mock; callbacks.validate_network_profile_callback = validate_network_profile_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -512,46 +512,46 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.configure_network_connection_profile_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction configure_network_connection_profile_callback_mock; callbacks.configure_network_connection_profile_callback = configure_network_connection_profile_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalTimeSyncCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.time_sync_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction time_sync_callback_mock; callbacks.time_sync_callback = time_sync_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalBootNotificationCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.boot_notification_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction boot_notification_callback_mock; callbacks.boot_notification_callback = boot_notification_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalOCPPMessagesCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.ocpp_messages_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction ocpp_messages_callback_mock; callbacks.ocpp_messages_callback = ocpp_messages_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -559,13 +559,13 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.cs_effective_operative_status_changed_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction cs_effective_operative_status_changed_callback_mock; callbacks.cs_effective_operative_status_changed_callback = cs_effective_operative_status_changed_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -573,13 +573,13 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.evse_effective_operative_status_changed_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction evse_effective_operative_status_changed_callback_mock; callbacks.evse_effective_operative_status_changed_callback = evse_effective_operative_status_changed_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -587,14 +587,14 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.get_customer_information_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction customer_certificate, const std::optional id_token, const std::optional> customer_identifier)> get_customer_information_callback_mock; callbacks.get_customer_information_callback = get_customer_information_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -602,14 +602,14 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.clear_customer_information_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction customer_certificate, const std::optional id_token, const std::optional> customer_identifier)> clear_customer_information_callback_mock; callbacks.clear_customer_information_callback = clear_customer_information_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -617,33 +617,33 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.all_connectors_unavailable_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction all_connectors_unavailable_callback_mock; callbacks.all_connectors_unavailable_callback = all_connectors_unavailable_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalDataTransferCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.data_transfer_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction data_transfer_callback_mock; callbacks.data_transfer_callback = data_transfer_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01FR02_CallbacksValidityChecksIfOptionalTransactionEventCallbackIsNotSetOrNotNull) { configure_callbacks_with_mocks(); callbacks.transaction_event_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction transaction_event_callback_mock; callbacks.transaction_event_callback = transaction_event_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, @@ -651,13 +651,13 @@ TEST_F(ChargepointTestFixtureV201, configure_callbacks_with_mocks(); callbacks.transaction_event_response_callback = nullptr; - EXPECT_FALSE(callbacks.all_callbacks_valid()); + EXPECT_FALSE(callbacks.all_callbacks_valid(device_model)); testing::MockFunction transaction_event_response_callback_mock; callbacks.transaction_event_response_callback = transaction_event_response_callback_mock.AsStdFunction(); - EXPECT_TRUE(callbacks.all_callbacks_valid()); + EXPECT_TRUE(callbacks.all_callbacks_valid(device_model)); } TEST_F(ChargepointTestFixtureV201, K01_SetChargingProfileRequest_ValidatesAndAddsProfile) {