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,13 @@
# Getting Started with OCPP2.0.1
## Integrate this library with your Charging Station Implementation for OCPP2.0.1
OCPP is a protocol that affects, controls and monitors many areas of a charging station's operation.
If you want to integrate this library with your charging station implementation, you have to register a couple of **callbacks** and integrate **event handlers**. This is necessary for the library to interact with your charging station according to the requirements of OCPP.
Libocpp needs registered **callbacks** in order to execute control commands defined within OCPP (e.g ResetRequest or RemoteStartTransactionRequest)
The implementation must call **event handlers** of libocpp so that the library can track the state of the charging station and trigger OCPP messages accordingly (e.g. MeterValuesRequest , StatusNotificationRequest)
Your reference within libocpp to interact is a single instance to the class ocpp::v2::ChargePoint defined in `v2/charge_point.hpp` for OCPP 2.0.1.

View File

@@ -0,0 +1,57 @@
# OCPP 2.0.1 and 2.1: Device model initialization and inserting of config values
If there is no custom database used for the device model, and 'initialize_device_model' is set to true in the
constructor of ChargePoint, the device model will be created or updated when ChargePoint is created. This document will
give more information about the files you need and what will be updated when the 'initialize_device_model' is set
to true.
## Database, component config and config file paths
Along with the 'initialize_device_model' flag, a few paths must be given to the constructor:
- The path of the device model migration files (normally `resources/v2/device_model_migration_files`).
- The path of the device model database.
- The path of the directory with the device model config. There should be two directories in it: 'standardized' and
'custom', both containing device model config.
## Component config
When the database is created for the first time, it will insert all components, variables, characteristics and
attributes from the component config.
## Update config values
Each time the ChargePoint class is instantiated, the component config is read and the values will be set to the database
accordingly. Only the initial values will be set to the values in the component config. So if for example the CSMS
changed a value, it will not be updated to the value from the component config file.
## Update component config
To update a component, just place the correct json component config in the `component_config/custom` or
`component_config/standardized` folder. When restarting the software, it will:
- Check if there are Components in the database that are not in the component config's. Those will be removed.
- Check if there are Components in the component config's that are not in the database. Those will be added.
- Check if anything has changed inside the Component (`Variable`, `Characteristics` or `Attributes`).
Those will be removed, changed or added to the database as well.
Note: When the `evse_id` or `connector_id` of a component is changed, this is seen as the removal of a Component and
addition of a new one.
Note: OCPP requires EVSE and Connector numbering starting from 1 counting upwards.
Note: There should be no duplicate components or variables in the component config files.
## Required variables
There are some required Variables, which can be found in the OCPP spec.
Some `Variables` are only required if the `Component` is `Available`, for example `Reservation` and `Smart Charging`.
There are some Components that are always required because that is how libocpp works: `AlignedDataCtrlr` and
`SampledDataCtrlr`.
When libocpp is started and initialized, all required Variables will be checked and an DeviceModelError is thrown if
one of the required Variables is not there.
This also implies, that if you write code that needs a required `Variable`, when trying to get that variable with
`DeviceModel::get_value(...)`, you should first check if the Component that Variable belongs to is `Available`.

View File

@@ -0,0 +1,76 @@
# OCPP 2.0.1: Monitors
Monitors are a mechanism for reporting based on certain criteria the internal state of the variables present on the charger. The monitors can be configured in different ways, with custom monitors being sent from the CSMS and HardWired and Preconfigured monitors set up in the config of the database.
## Basic Configuration
The monitors are evaluated from time to time in the case of periodic monitors and after a variable has been modified in the case of monitors that are triggered. Periodic monitors will be handled from time to time, the default being 1 second.
### Variables
- Enabling monitors: set the `MonitoringCtrlrEnabled` variable to true
- Periodic monitor process time: set the `MonitorsProcessingInterval` to the desired interval (default 1 second)
- To activate monitor processing: set the `ActiveMonitoringBase` variable to `All`
- To filter the verbosity level: set the `ActiveMonitoringLevel` variable to a value of 0-9 with 9 being the most verbose
- To filter the verbosity level when the charging station is offline: set the `OfflineQueuingSeverity` value to 0-9, with 9 keeping all monitor generated event while being offline
Note: There is a small overhead for the monitoring process interval. The periodic monitors that are triggered will require a database value query. However, based on the count and config of monitors it is unlikely that many of them will trigger at the same time, therefore, the database queries will be limited.
## Hardwired/Preconfigured Monitors
In order to set up pre-existing monitors that are not set up by the CSMS, for the variables that allow monitoring the configuration json file can be extended in the following way:
```json
"EVSEPower": {
"variable_name": "Power",
"characteristics": {
"unit": "W",
"maxLimit": 22000,
"supportsMonitoring": true,
"dataType": "decimal"
},
"attributes": [
{
"type": "Actual",
"mutability": "ReadOnly"
},
{
"type": "MaxSet",
"mutability": "ReadOnly"
}
],
"monitors": [
{
"value": 21950,
"severity": 1,
"transaction": false,
"type": "UpperThreshold",
"config_type": "HardWiredMonitor"
},
{
"value": 100,
"severity": 1,
"transaction": false,
"type": "LowerThreshold",
"config_type": "HardWiredMonitor"
},
{
"value": 100,
"severity": 1,
"transaction": false,
"type": "Delta",
"reference_value": "10700",
"config_type": "PreconfiguredMonitor"
}
],
"description": "",
"type": "number",
"default": "0"
}
```
In the example for the 'EVSEPower' variable that supports monitoring there were attached three hardwired and preconfigured monitors. The monitors will report (based on the setup, see the `Basic Configuration` section) to the CSMS when the power will exceed '21950' W, when the power will fall below '100' W and when there will be a delta difference of more than `100` W from the `reference_value` in the case of the delta monitor. When the delta is exceeded the `reference_value` will be updated internally, and a new delta will be calculated based on that.
For more information related to the monitor functionality, please refer to the OCPP201 specification.
Note: for a delta monitor, an initial `reference_value` must be provided or the library will fail to initialize.

View File

@@ -0,0 +1,37 @@
@startuml Periodic Monitors
start
:DB Monitor Config Setup;
:DB Monitor Initialization;
if (Monitoring Enabled?) then (yes)
:Query Config\nProcess Time;
:Enable Monitor\nProcessor;
repeat
:Verify Periodic\nMonitors;
if(Periodic Monitor\nTime Elapsed?) then (yes)
if(Online?) then (yes)
if(Active Monitoring Severity\n> Monitor Severity?) then (yes)
:Queue \nPeriodic Monitor;
else (no)
:Discard Monitor;
endif
else (no)
if(Offline Monitoring Severity\n> Monitor Severity?) then (yes)
:Queue \nPeriodic Monitor;
else (no)
:Discard Monitor;
endif
endif
else(no)
:Update Monitor\nElapsed Time;
endif
:Verify Triggered\nMonitors\n(see trigger flowchart);
:Queue Triggered\nMonitors;
:Send Queued\nMonitors to CSMS;
repeat while (Each N Seconds)
else (no)
:Do Nothing;
endif
stop
@enduml

View File

@@ -0,0 +1,35 @@
@startuml Periodic Monitors
start
:DB Monitor Config Setup;
:DB Monitor Initialization;
if (Monitoring Enabled?) then (yes)
:Inject Device\nModel Listener;
:Enable Monitor\nProcessor;
repeat
:Verify Variable\nMonitors;
if(Variable Monitor\nTriggered?) then (yes)
if(Online?) then (yes)
if(Active Monitoring Severity\n> Monitor Severity?) then (yes)
:Queue Monitor;
else (no)
:Discard Monitor;
endif
else (no)
if(Offline Monitoring Severity\n> Monitor Severity?) then (yes)
:Queue Monitor;
else (no)
:Discard Monitor;
endif
endif
else(no)
:Do Nothing;
endif
:Process Queued\nMonitors;
repeat while (DB Variable Updated)
else (no)
:Do Nothing;
endif
stop
@enduml

View File

@@ -0,0 +1,181 @@
# Smart Charging Use Cases
The use cases within the Smart Charging functional block are subdivided into the following three categories of use cases:
1. General Smart Charging (Use Cases K01K10)
2. External Charging Limit-based Smart Charging (K11K14)
3. ISO 15118-based Smart Charging (K15K17)
Support for General and External Charging Limit-based Smart Charging is largely complete, with ISO 15118-based Smart Charging under active development. For an up-to-date overview of exactly which features are currently supported as well as design decisions that have been made to address optional or ambiguous functional requirements, please refer to the [OCPP 2.0.1 Status document](ocpp_2x_status.md).
## K01 SetChargingProfile
Allows the CSMS to influence the charging power or current drawn from a specific EVSE or the
entire Charging Station over a period of time.
```mermaid
sequenceDiagram
CSMS->>+ChargePoint : SetChargingProfileRequest(call)
ChargePoint->>+DeviceModel : SmartChargingCtrlrAvailable?
DeviceModel-->>-ChargePoint : Component
rect rgb(128,202,255)
break SmartChargingCtrlrAvailable = false
ChargePoint-->>CSMS : Smart Charging NotSupported CallError
end
end
ChargePoint->>+SmartCharging : validate_and_add_profile(call.msg.Profile, call.msg.EVSE ID)
SmartCharging->>SmartCharging : validate_profile(Profile, EVSE ID)
rect rgb(128,202,255)
break Invalid Profile
SmartCharging-->>ChargePoint : SetChargingProfileResponse: Rejected
ChargePoint-->>CSMS : SetChargingProfileResponse: Rejected
end
end
SmartCharging->>+SmartCharging : add_profile(Profile, EVSE ID)
SmartCharging->>-EVerest : signal_set_charging_profiles_callback
SmartCharging-->>-ChargePoint : SetChargingProfileResponse: Accepted
ChargePoint-->>-CSMS : SetChargingProfileResponse: Accepted
```
Profile validation returns the following errors to the caller when a Profile
is `Rejected`:
| Errors | Description |
| :------------------------------------------------------------ | :-------------------------------------------------------------- |
| `ChargingProfileFirstStartScheduleIsNotZero` | The `startPeriod` of the first `chargingSchedulePeriod` needs to be 0.<br>[K01.FR.31] |
| `ChargingProfileNoChargingSchedulePeriods` | Happens when the `ChargingProfile` doesn't have any Charging Schedule Periods. |
| `ChargingScheduleChargingRateUnitUnsupported` | Happens when a chargingRateUnit is passed in that is not configured in the `ChargingScheduleChargingRateUnit`. [K01.FR.26] |
| `ChargingSchedulePeriodInvalidPhaseToUse` | Happens when an invalid `phaseToUse` is passed in. [K01.FR.19] [K01.FR.48] |
| `ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported` | Happens when `phaseToUse` is passed in and the EVSE does not have `ACPhaseSwitchingSupported` defined and set to true. [K01.FR.20] [K01.FR.48] |
| `ChargingSchedulePeriodsOutOfOrder` | `ChargingSchedulePeriod.startPeriod` elements need to be in increasing values. [K01.FR.35] |
| `ChargingStationMaxProfileCannotBeRelative` | Happens when a `ChargingStationMaxProfile.chargingProfileKind` is set to `Relative`. [K01.FR.38] |
| `ChargingStationMaxProfileEvseIdGreaterThanZero` | Happens when a `ChargingStationMaxProfile` is attempted to be set with an EvseID isn't `0`. [K01.FR.03] |
| `ChargingProfileMissingRequiredStartSchedule` | Happens when an `Absolute` or `Recurring` `ChargingProfile` doesn't have a `startSchedule`. [K01.FR.40] |
| `ChargingProfileExtraneousStartSchedule` | Happens when a Relative `ChargingProfile` has a `startSchedule`. [K01.FR.41] |
| `EvseDoesNotExist` | Happens when the `evseId`of a `SetChargingProfileRequest` does not exist. [K01.FR.28] |
| `ExistingChargingStationExternalConstraints` | Happens when a `SetChargingProfileRequest` Profile has a purpose of `ChargingStationExternalConstraints` and one already exists with the same `ChargingProfile.id` exists. [K01.FR.05] |
| `InvalidProfileType` | Happens when a `ChargingStationMaxProfile` is attempted to be set with a `ChargingProfile` that isn't a `ChargingStationMaxProfile`. |
| `TxProfileEvseHasNoActiveTransaction` | Happens when a `SetChargingProfileRequest` with a `TxProfile` is submitted and there is no transaction active on the specified EVSE. [K01.FR.09] |
| `TxProfileEvseIdNotGreaterThanZero` | `TxProfile` needs to have an `evseId` greater than 0. [K01.FR.16] |
| `TxProfileMissingTransactionId` | A `transactionId` is required for`SetChargingProfileRequest`s with a `TxProfile` in order to match the profile to a specific transation. [K01.FR.03] |
| `TxProfileTransactionNotOnEvse` | Happens when the provided `transactionId` is not known. [K01.FR.33] |
| `TxProfileConflictingStackLevel` | Happens when a `TxProfile` has a `stackLevel` and `transactionId` combination already exists in a `TxProfile` with a different id in order to ensure that no two charging profiles with same stack level and purpose can be valid at the same time. [K01.FR.39] |
## K08 Get Composite Schedule
The CSMS requests the Charging Station to report the Composite Charging
Schedule, as calculated by the Charging Station for a specific point of
time, and may change over time due to external causes such as local
balancing based on grid connection capacity and EVSE availablity.
The Composite Schedule is the result of result of merging the time periods
set in the `ChargingStationMaxProfile`, `ChargingStationExternalConstraints`,
`TxDefaultProfile` and `TxProfile` type profiles.
```mermaid
sequenceDiagram
CSMS->>+ChargePoint: GetCompositeSchedule(call)
ChargePoint->>+DeviceModel : ChargingScheduleChargingRateUnit?
DeviceModel-->>-ChargePoint : Component
rect rgb(128,202,255)
break call.msg.chargingRateUnit is not supported
ChargePoint-->>CSMS : ChargingScheduleChargingRateUnitUnsupported CallError
end
end
ChargePoint->>+EvseManager : does_evse_exist(call.msg.evseId)
EvseManager-->>-ChargePoint : bool
rect rgb(128,202,255)
break EVSE does not exist
ChargePoint-->>CSMS : EvseDoesNotExist CallError
end
end
ChargePoint->>+SmartChargingHandler : get_valid_profiles(call.msg.evseId)
SmartChargingHandler-->>-ChargePoint : vector<ChargingProfile>
ChargePoint->>+SmartChargingHandler : calculate_composite_schedule<br/>(vector<ChargingProfile, now, msg.duration, evseId, call.msg.chargingRateUnit)
loop ExternalConstraints, Max, TxDefault, and Tx Profiles
SmartChargingHandler->>+Profile: calculate_composite_schedule(profiles)
Profile-->>-SmartChargingHandler: composite_schedule
end
note right of SmartChargingHandler: Create consolidated CompositeSchedule<br />from all 4 Profile types
SmartChargingHandler->>+Profile: calculate_composite_schedule(ExternalConstraints, Max, TxDefault, Tx)
Profile-->>-SmartChargingHandler: CompositeSchedule
SmartChargingHandler-->>-ChargePoint: CompositeSchedule
ChargePoint-->>-CSMS : GetCompositeScheduleResponse(CompositeSchedule)
```
## K09 Get Charging Profiles
Returns to the CSMS the Charging Schedules/limits installed on a Charging Station based on the
passed in criteria.
```mermaid
sequenceDiagram
CSMS->>+ChargePoint: GetChargingProfiles(criteria)
ChargePoint->>+SmartChargingHandler : get_reported_profiles(criteria)
loop filter ChargingProfiles
SmartChargingHandler->>SmartChargingHandler: filter on ChargingProfile criteria
end
SmartChargingHandler-->>-ChargePoint : Vector<Profiles>
ChargePoint-->>CSMS : GetChargingProfilesResponse(profiles)
alt no Profiles
rect rgb(128,202,255)
ChargePoint-->>CSMS : GetChargingProfilesResponse(NoProfiles)
ChargePoint->>CSMS : return
end
else Profiles
ChargePoint-->>CSMS : GetChargingProfilesResponse(Accepted)
end
ChargePoint->>ChargePoint : determine profiles_to_report
ChargePoint-->>-CSMS : ReportChargingProfilesRequest(profiles_to_report)
```
## K10 Clear Charging Profile
Clears Charging Profiles installed on a Charging Station based on the
passed in criteria.
```mermaid
sequenceDiagram
CSMS->>+ChargePoint: ClearChargingProfileRequest(criteria)
alt no Profiles matching criteria
rect rgb(128,202,255)
ChargePoint-->>CSMS : ClearChargingProfileResponse(Unknown)
end
else found matching Profiles
ChargePoint-->>-CSMS : ClearChargingProfileResponse(Accepted)
end
```

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,142 @@
# R04 DER Control — Supersede Mechanism
This document explains how `SetDERControl` (OCPP 2.1, use case R04) decides
whether a newly received control supersedes existing controls of the same
`controlType` + `isDefault`, and when that supersede takes effect.
## Priority comparison
OCPP 2.1 R04 uses a **lower priority value wins** convention:
| `new.priority` vs `existing.priority` | Outcome |
|----|----|
| `new < existing` | new overrules existing (FR.03 / FR.06 / FR.07) |
| `new == existing` | tie does **not** supersede; existing keeps running |
| `new > existing` | existing overrules new; new is stored already-superseded (FR.08) |
The comparison only runs against rows that share the same `controlType` and
`isDefault`. Different control types coexist independently.
## FR.03 — default vs default
Defaults have no `startTime` / `duration`. They are conceptually always
"active" but only one default per `controlType` is in force at any time.
- New default with **strictly lower** `priority` value than the existing
default → existing is flagged `IS_SUPERSEDED = 1` immediately.
- New default with equal priority value → both rows persist, neither is
flagged. Existing keeps running.
- New default with strictly higher priority value → new row is stored with
`IS_SUPERSEDED = 1`; the existing default keeps running.
## FR.06 — existing not yet active
A scheduled control whose `startTime > now` is "not yet active". When a new
control overrules it (lower priority value), the supersede is **immediate**:
the existing row is flagged `IS_SUPERSEDED = 1` inside the same transaction
that persists the new row.
The response's `supersededIds` lists the existing `controlId`(s).
## FR.07 — existing currently active
When the existing control is currently active (`startTime <= now < startTime + duration`)
and the new control has a future `startTime`, the supersede is **deferred**:
- The new row is stored with `PENDING_SUPERSEDE_ID = <existing controlId>`.
- The existing row keeps running until `now >= new.startTime`.
- The 30 s periodic check fires the deferred flip the moment `new.startTime`
is reached: existing flips to `IS_SUPERSEDED = 1`, the new row's
`PENDING_SUPERSEDE_ID` is cleared, and `NotifyDERStartStop(started=true,
supersededIds=[existing])` is dispatched for the new control.
The `SetDERControl` response **omits** `supersededIds` for the deferred case.
The CSMS learns about the supersede via the later `NotifyDERStartStop`, not
synchronously.
```mermaid
sequenceDiagram
autonumber
participant CSMS
participant CS as CS (DERControl)
participant DB as DER_CONTROLS
Note over DB: existing: startTime <= now < expiry
CSMS->>CS: SetDERControl(new, startTime=T_future, prio < existing.prio)
CS->>DB: BEGIN TRANSACTION
CS->>DB: SELECT existing rows for controlType+isDefault
Note over CS: existing currently active + new has future startTime
CS->>DB: INSERT new row<br/>PENDING_SUPERSEDE_ID = existing.controlId
CS->>DB: COMMIT
CS-->>CSMS: SetDERControlResponse(Accepted)<br/>(no supersededIds yet)
Note over CS: existing keeps running until T_future...
Note over CS: 30 s periodic check<br/>now >= T_future
CS->>DB: BEGIN TRANSACTION
CS->>DB: existing.IS_SUPERSEDED = 1<br/>new.PENDING_SUPERSEDE_ID = NULL<br/>new.STARTED_NOTIFIED = 1
CS->>DB: COMMIT
CS-->>CSMS: NotifyDERStartStop(new, started=true,<br/>supersededIds=[existing])
```
## FR.08 — new is itself superseded
If any existing row of the same `controlType` + `isDefault` has **strictly
lower** priority value than the new control, the new control loses. It is
stored with `IS_SUPERSEDED = 1`, and the response's `supersededIds` echoes
the **new** `controlId` to tell the CSMS its just-set control will not run.
When this happens, any pending immediate or deferred supersede side effects
the new control would have caused against other rows are dropped — a row
that will never actually take effect must not flag other rows as superseded
by it.
```mermaid
sequenceDiagram
autonumber
participant CSMS
participant CS as CS (DERControl)
participant DB as DER_CONTROLS
Note over DB: existing: priority = 3 (high)
CSMS->>CS: SetDERControl(new, priority = 5 (low))
CS->>DB: BEGIN TRANSACTION
CS->>DB: SELECT existing rows
Note over CS: existing.priority < new.priority<br/>=> new is self-superseded
CS->>DB: INSERT new row<br/>IS_SUPERSEDED = 1
Note over CS: drop any pending supersede effects new would have caused
CS->>DB: COMMIT
CS-->>CSMS: SetDERControlResponse(Accepted,<br/>supersededIds=[new.controlId])
Note over DB: existing keeps running
```
## `displacedIds` persistence and FR.22 expiry-notify
Every row records the controlIds it has displaced in
`CONTROL_JSON.displacedIds`. The list is populated:
- at `SetDERControl` insert from the immediate-supersede path
(`pending_immediate_supersedes`), and
- at deferred-supersede activation via
`DatabaseHandler::append_der_control_displaced_id(new_id, existing_id)`.
When the row expires (`startTime + duration <= now`), the periodic check
emits `NotifyDERStartStopRequest(started=false, supersededIds=<displacedIds>)`
so the CSMS receives the same supersede relationship on expiry that it saw on
the matching start notify. The field is optional per the OCPP 2.1 Part 2
§1.48.1 schema (`0..24`), so emitting it on `started=false` is permitted.
## Side-effect resolution order
`handle_set_der_control` collects supersede side effects in two slots before
applying them:
- `pending_immediate_supersedes` — list of existing `controlId`s the new row
would flip to `IS_SUPERSEDED = 1` synchronously (FR.03 / FR.06 path).
- `deferred_supersede_target` — the single existing `controlId` the new row
defers superseding via `PENDING_SUPERSEDE_ID` (FR.07 path).
If during the scan a higher-priority existing row is found (`new_is_superseded =
true`, FR.08), both slots are cleared before any DB write. Otherwise, the
immediate flips are applied and `superseded_ids` is populated for the
response. The single transaction wrapping the whole handler ensures no
concurrent CSMS call or scheduled-check pass can observe a half-applied
state.

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1,78 @@
@startuml
'https://plantuml.com/sequence-diagram
!pragma teoz true
actor user order 10
participant core order 20
participant libocpp order 30
database database order 40
participant csms order 50
autonumber "<b><font color=red>"
skinparam sequenceArrowThickness 2
== Start a transaction ==
user->core: Swipe ID tag
core->libocpp ++: Authorize
libocpp-->core --: Authorized
user->core: Plug in cable
core->libocpp: on_transaction_started()
libocpp->csms: TransactionEventRequest::Started
libocpp->database: Insert transaction details
== Charging state changed (Example) ==
user->core: Pauses charging of car
core->libocpp: on_charging_state_changed()
libocpp->csms: TransactionEventRequest::Updated
libocpp->database: Update transaction charging state
libocpp->database: Update transaction sequence number
== Power Outage / Crash of libocpp ==
core->libocpp ++ : Construct ChargePoint
loop For each evse_id
alt Database does not contain transaction
libocpp->database ++: Get transaction for evse_id
database-->libocpp --: Return empty
else Database contains transaction
libocpp->database ++: Get transaction for evse_id
database-->libocpp --: Return transaction details
libocpp->libocpp: Re-initialize transaction for evse_id
end
end
libocpp-->core -- : Construction complete
note over core,database: Continue normal operations
user->core: Continues charging of car
core->libocpp: on_charging_state_changed()
alt No support for resuming transactions
libocpp->libocpp: Print warning of no know transaction
else With support for resuming transactions
libocpp->csms: TransactionEventRequest::Updated
libocpp->database: Update transaction charging state
libocpp->database: Update transaction sequence number
end
user->core: Swipe ID tag
core->libocpp: on_transaction_finished()
alt No support for resuming transactions
libocpp->libocpp: Print warning of no know transaction
else With support for resuming transactions
libocpp->csms: TransactionEventRequest::Ended
libocpp->database: Delete transaction
end
@enduml