Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,109 @@
# 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 `TariffMessage`, defined in
`common/types.hpp`. In case of multi language messages, they are all added to the TariffMessage vector.
In order to be able to use information from `SetUserPrice` before a transaction is started (e.g. for the OCMF TT field), OCPP1.6 implements a mechanism to wait for `DataTransfer.req(SetUserPrice)` from
the CSMS before the `authorize_id_token` function returns. This is required because the `Authorize.conf` in OCPP1.6 does not contain this information (unlike OCPP2.x). This allows to integrate the information
from `DataTransfer.req(SetUserPrice)` in the response of the authorization request. The timeout for waiting for this message from the CSMS can be controlled using the `WaitForSetUserPriceTimeout`
configuration key, which is specified in milliseconds. If no `DataTransfer.req(SetUserPrice)` is received within the specified timeout, the result is returned and a transaction may still start.
If the message is sent when a transaction has already started, the session id will be included in the session cost 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,\<language code\>"`, 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
- `tariff_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 described above. For California Pricing Requirements to work, 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` | `<language code>` | `TariffCostCtrlr` | TariffFallbackMessage in a specific language. There must be a variable with the language as instance for every supported language. |
| `OfflineTariffFallbackMessage` | `<language code>` | `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` | `<language code>` | `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 TariffMessage
(`tariff_message_callback`) containing the personal message(s).
### Callbacks
For California Pricing to work, the following callbacks must be enabled:
- `set_running_cost_callback`
- `tariff_message_callback`
For the tariff information (the personal messages), the `tariff_message_callback` is used.
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 TariffMessage is sent.

View File

@@ -0,0 +1,13 @@
# Exception Handling of Database Operations
OCPP versions 1.6 and 2.0.1 contain several requirements for the persistent storage and retrieval of data. This library leverages SQLite to fulfill these persistent storage requirements. The DatabaseHandler classes manage all related database operations, including CREATE, SELECT, INSERT, UPDATE, and DELETE commands.
## Exception Design Considerations
The primary design decision in the development of DatabaseHandler classes is to allow exceptions to propagate to higher level logic rather than catching them internally. By propagating exceptions of database operations, it is ensured the consumer application is fully aware of any issues that occur during database operations. This transparency allows higher-level logic to make informed decisions based on the specific errors encountered. Different higher level logic may require different strategies for error handling based on their specific requirements. Propagating exceptions gives developers the flexibility to implement custom handling procedures that best fit the applications needs.
SQLite does not natively support exceptions as it is written in C. The DatabaseHandler classes check the return codes of SQLite function calls, and if an operation fails (i.e., the return code is not SQLITE_OK, SQLITE_DONE, SQLITE_ROW, etc.), an exception is thrown. This way, all database-related errors are converted into C++ exceptions, which are then propagated up to the caller.
## Implementation Strategy
The public functions of the DatabaseHandler classes are designed to throw exceptions when SQL operations fail to execute as expected Error Handling. Consumer code should implement try-catch blocks around calls to DatabaseHandler functions. This handling should be tailored to the requirements of the OCPP specification and improve the stability of the application, whether it involves the logging errors, retrying operations or other logic.

View File

@@ -0,0 +1,3 @@
# Database migrations
The migrations support has been moved to [everest-sqlite](https://github.com/EVerest/EVerest/tree/main/lib/everest/sqlite). Please see its [documentation for the migration support](https://github.com/EVerest/EVerest/tree/main/lib/everest/sqlite/docs/migrations.md).

View File

@@ -0,0 +1,72 @@
# Getting Started
## Requirements
For Debian GNU/Linux 11 you will need the following dependencies:
```bash
sudo apt install build-essential cmake python3-pip libboost-all-dev libsqlite3-dev libssl-dev
```
OpenSSL version 3.0 or above is required.
Clone this repository.
```bash
git clone https://github.com/EVerest/libocpp
```
In the libocpp folder create a folder named build and cd into it.
Execute cmake and then make:
```bash
mkdir build && cd build
cmake ..
make -j$(nproc)
```
## Unit testing
GTest is required for building the test cases target.
To build the target and run the tests you can reference the script `.ci/build-kit/install_and_test.sh`.
The script allows the GitHub Actions runner to execute.
Local testing:
```bash
mkdir build
cmake -B build -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="./dist"
cd build
make -j$(nproc) install
```
Run any required tests from build/tests.
## Clarifications for directory structures, namespaces and OCPP versions
This repository contains multiple subdirectories and namespaces named v16, v2 and v21.
* The v16 directories contain files for configuring and implementing OCPP 1.6.
* The v2 directories include files for both OCPP2.0.1 and OCPP2.1, as OCPP 2.1
is fully backward compatible with OCPP 2.0.1.
* The v21 directories include files only for OCPP2.1.
## Get Started with OCPP1.6
Please see the [Getting Started documentation for OCPP1.6](../v16/getting_started.md).
## Get Started with OCPP2.0.1
Please see the [Getting Started documentation for OCPP2.0.1](../v2/getting_started.md).
## Building the doxygen documentation
```bash
cmake -S . -B build
cmake --build build --target doxygen-ocpp
```
You will find the generated doxygen documentation at:
`build/dist/docs/html/index.html`
The main reference for the integration of libocpp for OCPP1.6 is the ocpp::v16::ChargePoint class defined in `v16/charge_point.hpp` , for OCPP2.0.1 and OCPP2.1 that is the ocpp::v2::ChargePoint class defined in `v2/charge_point.hpp` .

View File

@@ -0,0 +1,244 @@
# Notes on Smart Charging Profiles and approach
There are some complexities calculating composite schedules and this note explains the approach.
The new approach is in `profile.cpp` and `profile.hpp` and then integrated into `smart_charging.cpp` maintaining the same API.
```cpp
std::vector<ChargingProfile> get_valid_profiles(
const DateTime& start_time, const DateTime& end_time,
int connector_id);
EnhancedChargingSchedule calculate_enhanced_composite_schedule(
const std::vector<ChargingProfile>& valid_profiles,
const DateTime& start_time,
const DateTime& end_time,
const int connector_id,
std::optional<ChargingRateUnit> charging_rate_unit);
```
## get_valid_profiles()
Retrieves all profiles that should be considered for calculating the composite schedule.
- checks that profiles are associated with the correct connector
- checks that profiles match the transaction ID (when there is an active transaction)
- start_time and end_time are not used in the new implementation
`start_time` and `end_time` could be used to remove profiles that would never be valid
within that period. However only the `validFrom` and `validTo` settings should be considered.
`calculate_enhanced_composite_schedule` checks the `start_time` and `end_time` so it is not essential to remove profiles at this point.
## calculate_enhanced_composite_schedule()
Assumes that profiles for other connectors and transactions have been removed.
Processes profiles:
1. obtain session/transaction start time
2. split the profiles into ChargePointMax/TxDefault/Tx sets
3. for each set calculate the composite schedule using the preferred units (Amps/Watts)
the resulting composite schedule covers the time period `start_time` to `end_time` only. Note that stack level is used where the higher level has priority
4. combines the three composite schedules into the single result
When combining; TxDefault is the starting limit, overridden by Tx and potentially lowered by ChargePointMax. The result will never be higher than a ChargePointMax limit (where there is one).
## Time handling
The approach removes all calls for obtaining the current date and time.
- where there is a transaction then the start time of the transaction is obtained
- relative schedules use the transaction start time where known or the start time
For generating a composite schedule relative schedules are included based on the transaction start time. Where there isn't a transaction in progress the `start_time` is used - i.e. the assumption is that a transaction has just started.
The removal of any relationship to the current time simplifies writing test cases and debugging test failures.
## Default limit
The OCPP 1.6 specification doesn't support gaps in charging schedules. This presents a problem while creating a composite schedule when there is a period of time when no profile is active.
- profile 1: stack level 10, Monday 10:00 for 2 hours
- profile 2: stack level 11, Monday 14:00 for 2 hours
At Monday 08:00 requesting a composite schedule for the next 9 hours needs to indicate
08:00|09:00|10:00|11:00|12:00|13:00|14:00|15:00|16:00|17:00
-----|-----|-----|-----|-----|-----|-----|-----|-----|-----
unknown|unknown|P1|P1|unknown|unknown|P2|P2|unknown|unknown|unknown
Where the limit is not known then a default limit of `48.0 Amps` is used when calculating the final composite schedule.
A different default can be specified by installing a lower stack level TxDefault profile e.g.
```json
{
"chargingProfileId": 1,
"chargingProfileKind": "Relative",
"chargingProfilePurpose": "TxDefaultProfile",
"stackLevel": 1
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"limit": 32.0,
"startPeriod": 0
}
],
},
}
```
## No valid profiles
Since a default limit is applied a composite schedule will always start at the `start_time` and have a fixed duration even when there are no valid profiles for that time period.
e.g. for 2024-01-01 starting at 08:00 for 10 minutes
```json
{
"status": "Accepted",
"scheduleStart": "2024-01-01T08:00:00Z",
"chargingSchedule": {
"duration:": 3600,
"startSchedule": "2024-01-01T08:00:00Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": {
"startPeriod": 0,
"limit": 0.0
}
}
}
```
When building the OCPP response "startSchedule" could be excluded however the composite schedule refers to a specific point in time, so it should be provided.
According to the OCPP specification section `7.13 ChargingSchedule` chargingSchedulePeriod is a required field and must have at least one entry. Hence the following is not a valid response:
```json
{
"status": "Accepted",
"scheduleStart": "2024-01-01T08:00:00Z",
"chargingSchedule": {
"duration:": 3600,
"startSchedule": "2024-01-01T08:00:00Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": {}
}
}
```
## Profile validity
The following items need to be considered when looking at a schedule:
- validFrom
- validTo
- transaction start time
- startSchedule
- duration
- startPeriod
The following sections explore some interesting edge cases.
### validFrom & validTo and transaction start time
For the following schedule
```json
{
"chargingProfileId": 1,
"chargingProfileKind": "Relative",
"chargingProfilePurpose": "TxDefaultProfile",
"stackLevel": 1,
"chargingSchedule": {
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"limit": 32.0,
"startPeriod": 0
},
{
"limit": 6.0,
"startPeriod": 3600
}
],
},
"validFrom": "2024-01-01T12:00:00Z",
"validTo": "2024-01-01T20:00:00Z"
}
```
The PEV plugs in at "2024-01-01T10:00:00Z" the result is no charging until the profile is valid at 12:00 and then the limit is 6.0A since only the first hour of a transaction is at the higher limit of 32.0A
The PEV plugs in at "2024-01-01T19:50:00Z" the result is charging for 10 minutes at 32. A and then no charge offered.
### startSchedule and daily recurring
For the following schedule
```json
{
"chargingProfileId": 1,
"chargingProfileKind": "Recurring",
"chargingProfilePurpose": "TxDefaultProfile",
"recurrencyKind": "Daily",
"stackLevel": 1,
"chargingSchedule": {
"startSchedule": "2024-01-01T12:00:00Z",
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"limit": 32.0,
"startPeriod": 0
},
{
"limit": 6.0,
"startPeriod": 3600
}
],
},
}
```
The PEV plugs in at "2024-01-10T11:50:00Z" the result is charging at 6.0A for 10 minutes (based on the previous day) and then at 32.0A from 12:00 when the schedule repeats.
`validFrom` and `validTo` add additional complications.
### startSchedule and daily recurring with duration
For the following schedule
```json
{
"chargingProfileId": 1,
"chargingProfileKind": "Recurring",
"chargingProfilePurpose": "TxDefaultProfile",
"recurrencyKind": "Daily",
"stackLevel": 1,
"chargingSchedule": {
"startSchedule": "2024-01-01T12:00:00Z",
"duration": 18000,
"chargingRateUnit": "A",
"chargingSchedulePeriod": [
{
"limit": 32.0,
"startPeriod": 0
},
{
"limit": 6.0,
"startPeriod": 3600
}
],
},
"validFrom": "2024-02-01T12:00:00Z",
"validTo": "2024-03-01T09:00:00Z"
}
```
The PEV plugs in at "2024-02-10T11:50:00Z" the result is based on the default limit since the profile is only valid for 5 hours. i.e. no charging for 10 minutes (based on the previous day) and then at 32.0A from 12:00 when the schedule repeats.
The PEV plugs in at "2024-02-01T11:50:00Z" the result is based on the default limit since the profile isn't valid yet. i.e. no charging for 10 minutes and then at 32.0A from 12:00 when the schedule starts.
The PEV plugs in at "2024-03-01T08:00:00Z" the result is charging at 6.0A (based on the previous day) and then no charging at 09:00 when the profile is no longer valid.