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,17 @@
add_executable(charge_point charge_point.cpp)
target_link_libraries(charge_point
PRIVATE
Boost::thread
Boost::program_options
nlohmann_json::nlohmann_json
nlohmann_json_schema_validator
ocpp
OpenSSL::SSL
OpenSSL::Crypto
SQLite::SQLite3
)
install(TARGETS charge_point
RUNTIME)
set_property(TARGET ocpp PROPERTY POSITION_INDEPENDENT_CODE ON)

View File

@@ -0,0 +1,321 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <date/date.h>
#include <date/tz.h>
#include <fstream>
#include <iostream>
#include <mutex>
#include <sys/prctl.h>
#include <thread>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/exception/diagnostic_information.hpp>
#include <boost/program_options.hpp>
#include <everest/logging.hpp>
#include <ocpp/v16/charge_point.hpp>
#include <ocpp/v16/database_handler.hpp>
#include <ocpp/common/cistring.hpp>
#include <ocpp/common/string.hpp>
#include <ocpp/common/support_older_cpp_versions.hpp>
namespace po = boost::program_options;
ocpp::v16::ChargePoint* charge_point;
bool running = false;
std::mutex m;
int main(int argc, char* argv[]) {
po::options_description desc("OCPP charge point");
desc.add_options()("help,h", "produce help message");
desc.add_options()("maindir", po::value<std::string>(), "set main dir in which the schemas folder resides");
desc.add_options()("conf", po::value<std::string>(), "charge point config relative to maindir");
desc.add_options()("logconf", po::value<std::string>(), "The path to a custom logging.ini");
po::variables_map vm;
po::store(po::parse_command_line(argc, argv, desc), vm);
po::notify(vm);
if (vm.count("help") != 0) {
std::cout << desc << "\n";
return 1;
}
std::string maindir = ".";
if (vm.count("maindir") != 0) {
maindir = vm["maindir"].as<std::string>();
}
const auto database_path = "/tmp/ocpp";
const auto share_path = fs::path(maindir) / "share" / "everest" / "modules" / "OCPP";
// initialize logging as early as possible
auto logging_config = share_path / "logging.ini";
if (vm.count("logconf") != 0) {
logging_config = fs::path(vm["logconf"].as<std::string>());
}
Everest::Logging::init(logging_config.string(), "charge_point");
std::string conf = "config.json";
if (vm.count("conf") != 0) {
conf = vm["conf"].as<std::string>();
}
fs::path config_path = share_path / conf;
if (!fs::exists(config_path)) {
EVLOG_error << "Could not find config at: " << config_path;
return 1;
}
std::ifstream ifs(config_path.c_str());
std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
auto json_config = json::parse(config_file);
json_config["Internal"]["LogMessagesFormat"][0] = "console_detailed";
auto user_config_path = fs::path("/tmp") / "user_config.json";
if (fs::exists(user_config_path)) {
std::ifstream ifs(user_config_path.c_str());
std::string user_config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
auto user_config = json::parse(user_config_file);
json_config.merge_patch(user_config);
} else {
std::ofstream fs(user_config_path.c_str());
auto user_config = json::object();
fs << user_config << std::endl;
fs.close();
}
const fs::path sql_init_path = share_path / "core_migrations";
// create the cso_path
const fs::path cso_path = "/tmp/client/cso";
if (!fs::exists(cso_path)) {
fs::create_directories(cso_path);
}
const fs::path csms_path = "/tmp/client/csms";
if (!fs::exists(csms_path)) {
fs::create_directories(csms_path);
}
const fs::path ca_v2g_path = "/tmp/certs/ca/v2g/";
if (!fs::exists(ca_v2g_path)) {
fs::create_directories(ca_v2g_path);
}
const fs::path ca_mo_path = "/tmp/certs/ca/mo/";
if (!fs::exists(ca_mo_path)) {
fs::create_directories(ca_mo_path);
}
if (!fs::is_regular_file("/tmp/certs/ca/v2g/V2G_CA_BUNDLE.pem")) {
fsstd::ofstream("/tmp/certs/ca/v2g/V2G_CA_BUNDLE.pem");
}
if (!fs::is_regular_file("/tmp/certs/ca/mo/MO_CA_BUNDLE.pem")) {
fsstd::ofstream("/tmp/certs/ca/mo//MO_CA_BUNDLE.pem");
}
ocpp::SecurityConfiguration secConfig;
secConfig.csms_ca_bundle = fs::path("/tmp/certs/ca/v2g/V2G_CA_BUNDLE.pem");
secConfig.mf_ca_bundle = fs::path("/tmp/certs/ca/v2g/V2G_CA_BUNDLE.pem");
secConfig.v2g_ca_bundle = fs::path("/tmp/certs/ca/v2g/V2G_CA_BUNDLE.pem");
secConfig.mo_ca_bundle = fs::path("/tmp/certs/ca/mo/MO_CA_BUNDLE.pem");
secConfig.csms_leaf_cert_directory = fs::path("/tmp/client/csms/");
secConfig.csms_leaf_key_directory = fs::path("/tmp/client/csms/");
secConfig.secc_leaf_cert_directory = fs::path("/tmp/client/cso/");
secConfig.secc_leaf_key_directory = fs::path("/tmp/client/cso/");
charge_point = new ocpp::v16::ChargePoint(json_config.dump(), share_path, user_config_path, database_path,
sql_init_path, fs::path("/tmp"), nullptr, secConfig);
/************************************** START REGISTERING CALLBACKS **************************************/
charge_point->register_enable_evse_callback([](std::int32_t connector) {
std::cout << "Callback: "
<< "Enabling chargepoint at connector#" << connector;
return true;
});
charge_point->register_disable_evse_callback([](std::int32_t connector) {
std::cout << "Callback: "
<< "Disabling chargepoint at connector#" << connector;
return true;
});
charge_point->register_pause_charging_callback([](std::int32_t connector) {
std::cout << "Callback: "
<< "Pausing charging at connector#" << connector;
return true;
});
charge_point->register_resume_charging_callback([](std::int32_t connector) {
std::cout << "Callback: "
<< "Resuming charging at connector#" << connector;
return true;
});
charge_point->register_provide_token_callback(
[](const std::string& id_token, const std::vector<std::int32_t>& referenced_connectors, bool prevalidated) {
std::cout << "Callback: "
<< "Received token " << id_token << " for further validation" << std::endl;
});
charge_point->register_stop_transaction_callback([](std::int32_t connector, ocpp::v16::Reason reason) {
std::cout << "Callback: "
<< "Stopping transaction with at connector#" << connector
<< " with reason: " << ocpp::v16::conversions::reason_to_string(reason);
return true;
});
charge_point->register_reserve_now_callback([](std::int32_t reservation_id, std::int32_t connector,
ocpp::DateTime expiryDate, ocpp::CiString<20> idTag,
std::optional<ocpp::CiString<20>> parent_id) {
std::cout << "Callback: "
<< "Reserving connector# " << connector << " for id token: " << idTag.get() << " until "
<< expiryDate;
return ocpp::v16::ReservationStatus::Accepted;
});
charge_point->register_cancel_reservation_callback([](std::int32_t reservation_id) {
std::cout << "Callback: "
<< "Cancelling reservation with id: " << reservation_id;
return true;
});
charge_point->register_unlock_connector_callback([](std::int32_t connector) {
std::cout << "Callback: "
<< "Unlock connector#" << connector;
return ocpp::v16::UnlockStatus::Unlocked;
});
charge_point->register_upload_diagnostics_callback([](const ocpp::v16::GetDiagnosticsRequest& request) {
std::cout << "Callback: "
<< "Uploading Diagnostics file" << std::endl;
ocpp::v16::GetLogResponse r;
r.status = ocpp::v16::LogStatusEnumType::Accepted;
return r;
});
charge_point->register_update_firmware_callback([](const ocpp::v16::UpdateFirmwareRequest msg) {
std::cout << "Callback: "
<< "Updating Firmware" << std::endl;
});
charge_point->register_signed_update_firmware_callback([](ocpp::v16::SignedUpdateFirmwareRequest msg) {
std::cout << "Callback: "
<< "Updating Signed Firmware" << std::endl;
return ocpp::v16::UpdateFirmwareStatusEnumType::Accepted;
});
charge_point->register_upload_logs_callback([](ocpp::v16::GetLogRequest req) {
std::cout << "Callback: "
<< "Uploading Logs" << std::endl;
ocpp::v16::GetLogResponse r;
r.status = ocpp::v16::LogStatusEnumType::Accepted;
return r;
});
charge_point->register_set_connection_timeout_callback([](std::int32_t connection_timeout) {
std::cout << "Callback: "
<< "Setting connection timeout" << std::endl;
});
charge_point->register_reset_callback([](const ocpp::v16::ResetType& reset_type) {
std::cout << "Callback: "
<< "Exectuting: " << ocpp::v16::conversions::reset_type_to_string(reset_type) << " reset"
<< std::endl;
return true;
});
charge_point->register_set_system_time_callback([](const std::string& system_time) {
std::cout << "Callback: "
<< "Setting system time to " << system_time;
});
charge_point->register_signal_set_charging_profiles_callback([]() {
std::cout << "Callback: "
<< "Setting charging profiles" << std::endl;
});
charge_point->register_transaction_updated_callback([](const std::int32_t connector, const std::string& session_id,
const std::int32_t transaction_id,
const ocpp::v16::IdTagInfo& id_tag_info) {
std::cout << "Callback: Transaction updated at connector# " << connector
<< " and transaction id: " << transaction_id << std::endl;
});
/************************************** STOP REGISTERING CALLBACKS **************************************/
charge_point->start();
{
std::lock_guard<std::mutex> lk(m);
running = true;
}
std::thread t1([&] {
std::int32_t value = 0;
bool transaction_running = false;
while (!std::cin.fail() && running) {
std::string command;
std::cin >> command;
std::cout << std::endl;
std::string uuid;
if (command == "auth") {
auto result = charge_point->authorize_id_token(ocpp::CiString<20>(std::string("DEADBEEF")));
} else if (command == "start_transaction") {
if (!transaction_running) {
uuid = boost::lexical_cast<std::string>(boost::uuids::random_generator()());
charge_point->on_session_started(1, uuid, ocpp::SessionStartedReason::EVConnected, std::nullopt);
const auto result = charge_point->authorize_id_token(ocpp::CiString<20>(std::string("DEADBEEF")));
if (result.id_tag_info.status == ocpp::v16::AuthorizationStatus::Accepted) {
charge_point->on_transaction_started(1, uuid, "DEADBEEF", 0, std::nullopt, ocpp::DateTime(),
std::nullopt);
charge_point->on_resume_charging(1);
transaction_running = true;
}
}
} else if (command == "stop_transaction") {
if (transaction_running) {
charge_point->on_transaction_stopped(1, uuid, ocpp::v16::Reason::Local, ocpp::DateTime(), 2500,
std::nullopt, std::nullopt);
charge_point->on_session_stopped(1, uuid);
transaction_running = false;
} else {
std::cout << "No transaction is currently running. Use command \"start_transaction\" first"
<< std::endl;
}
} else if (command == "end") {
running = false;
} else if (command == "help") {
std::cout << "You can run the following commands:\n"
<< "\t- start_transaction : Starts a transaction with token \"DEADBEEF\"\n"
<< "\t- stop_transaction : Stops a transaction if currently running\n"
<< "\t- auth : Authorizes the token \"DEADBEEF\"\n"
<< "\t- end : Stop execution of the program\n"
<< std::endl;
} else {
std::cout << "Received unknown command" << std::endl;
}
}
});
t1.join();
std::cout << "Goodbye" << std::endl;
return 0;
}

View File

@@ -0,0 +1,37 @@
# Libocpp Code Generators
In this directory a collection of code generators for various purposes are located.
## C++ Code generator for v16 and v2
The script [generate_cpp.py](common/generate_cpp.py) can be used to generate datatypes, enums, and messages for v16 and v2.
```bash
python3 generate_cpp.py --schemas <json-schema-dir> --out <path-to-libocpp> --version <ocpp-version>
```
e.g.
```bash
python3 generate_cpp.py --schemas ~/ocpp-schemas/v16/ --out ~/checkout/everest-workspace/libocpp --version v16
```
```bash
python3 generate_cpp.py --schemas ~/ocpp-schemas/v2/ --out ~/checkout/everest-workspace/libocpp --version v2
```
```bash
python3 generate_cpp.py --schemas ~/ocpp-schemas/v21/ --out ~/checkout/everest-workspace/libocpp --version v21
```
## YAML code generator for EVerest types
The script [generate_everest_types.py](common/generate_cpp.py) can be used to generate EVerest YAML type definitions using OCPP2.0.1 and OCPP2.1 JSON schemas.
```bash
python3 generate_everest_types.py --schemas <json-schema-dir> --out <outfile> --types <comma-seperated-list-of-ocpp-types>
```
e.g.
```bash
python3 generate_everest_types.py --schemas ~/ocpp-schemas/v21/ --out ocpp.yaml --types ChargingNeedsType,ChargingScheduleType
```

View File

@@ -0,0 +1,709 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
#
"""OCPP JSON schema to cpp converter."""
from jinja2 import Environment, FileSystemLoader, select_autoescape
import json
import stringcase
from typing import Dict, List, Tuple
import keyword
from datetime import datetime
import argparse
from pathlib import Path
import subprocess
from utils import snake_case
def remove_last(input_string: str, search: str) -> str:
"""Removes the last occurence of search"""
if input_string.endswith(search):
return input_string[:input_string.rfind(search)]
return input_string
def uses_optional(types):
for t in types:
for property in t['properties']:
if not property['required']:
return True
return False
def needs_enums(types):
for t in types:
for property in t['properties']:
if property['enum']:
return True
return False
def needs_types(types):
type_list = ['CiString<6>', 'CiString<8>', 'CiString<16>', 'CiString<20>', 'CiString<25>', 'CiString<32>', 'CiString<36>',
'CiString<50>', 'CiString<64>', 'CiString<255>', 'CiString<500>', 'CiString<1000>', 'CiString<2500>', 'CiString<5600>', 'CiString<7500>', 'DateTime']
for t in types:
for property in t['properties']:
if property['type'] in type_list:
return True
return False
# jinja template environment and global variable
relative_tmpl_path = Path(__file__).resolve().parent / "templates"
env = Environment(
loader=FileSystemLoader(relative_tmpl_path),
trim_blocks=True,
autoescape=select_autoescape(
enabled_extensions=('html'))
)
env.filters['snake_case'] = snake_case
env.filters['remove_last'] = remove_last
env.globals['timestamp'] = datetime.utcnow
env.globals['year'] = datetime.utcnow().year
message_hpp_template = env.get_template('message.hpp.jinja')
message_cpp_template = env.get_template('message.cpp.jinja')
messages_cmakelists_txt_template = env.get_template(
'messages.cmakelists.txt.jinja')
enums_hpp_template = env.get_template('ocpp_enums.hpp.jinja')
enums_cpp_template = env.get_template('ocpp_enums.cpp.jinja')
ocpp_types_hpp_template = env.get_template('ocpp_types.hpp.jinja')
ocpp_types_cpp_template = env.get_template('ocpp_types.cpp.jinja')
# global variables, should go into a class
parsed_types: List = []
parsed_types_unique: List = []
parsed_enums: List = []
parsed_enums_unique: List = []
current_defs: Dict = {}
unique_types = set()
format_types = dict()
format_types['date-time'] = 'ocpp::DateTime'
format_types['uri'] = 'std::string' # FIXME(kai): add proper URI type
enum_types = dict()
enum_types['IdTagInfo'] = dict()
enum_types['IdTagInfo']['status'] = 'AuthorizationStatus'
enum_types['BootNotificationResponse'] = dict()
enum_types['BootNotificationResponse']['status'] = 'RegistrationStatus'
enum_types['CancelReservationResponse'] = dict()
enum_types['CancelReservationResponse']['status'] = 'CancelReservationStatus'
enum_types['ChangeAvailabilityRequest'] = dict()
enum_types['ChangeAvailabilityRequest']['type'] = 'AvailabilityType'
enum_types['ChangeAvailabilityResponse'] = dict()
enum_types['ChangeAvailabilityResponse']['status'] = 'AvailabilityStatus'
enum_types['ChangeConfigurationResponse'] = dict()
enum_types['ChangeConfigurationResponse']['status'] = 'ConfigurationStatus'
enum_types['ClearCacheResponse'] = dict()
enum_types['ClearCacheResponse']['status'] = 'ClearCacheStatus'
enum_types['ClearChargingProfileRequest'] = dict()
enum_types['ClearChargingProfileRequest']['chargingProfilePurpose'] = 'ChargingProfilePurposeType'
enum_types['ClearChargingProfileResponse'] = dict()
enum_types['ClearChargingProfileResponse']['status'] = 'ClearChargingProfileStatus'
enum_types['DataTransferResponse'] = dict()
enum_types['DataTransferResponse']['status'] = 'DataTransferStatus'
enum_types['DiagnosticsStatusNotificationRequest'] = dict()
enum_types['DiagnosticsStatusNotificationRequest']['status'] = 'DiagnosticsStatus'
enum_types['FirmwareStatusNotificationRequest'] = dict()
enum_types['FirmwareStatusNotificationRequest']['status'] = 'FirmwareStatus'
enum_types['GetCompositeScheduleRequest'] = dict()
enum_types['GetCompositeScheduleRequest']['chargingRateUnit'] = 'ChargingRateUnit'
enum_types['GetCompositeScheduleResponse'] = dict()
enum_types['GetCompositeScheduleResponse']['status'] = 'GetCompositeScheduleStatus'
enum_types['SampledValue'] = dict()
enum_types['SampledValue']['context'] = 'ReadingContext'
enum_types['SampledValue']['format'] = 'ValueFormat'
enum_types['SampledValue']['measurand'] = 'Measurand'
enum_types['SampledValue']['phase'] = 'Phase'
enum_types['SampledValue']['location'] = 'Location'
enum_types['SampledValue']['unit'] = 'UnitOfMeasure'
enum_types['ChargingProfile'] = dict()
enum_types['ChargingProfile']['chargingProfilePurpose'] = 'ChargingProfilePurposeType'
enum_types['ChargingProfile']['chargingProfileKind'] = 'ChargingProfileKindType'
enum_types['ChargingProfile']['recurrencyKind'] = 'RecurrencyKindType'
enum_types['RemoteStartTransactionResponse'] = dict()
enum_types['RemoteStartTransactionResponse']['status'] = 'RemoteStartStopStatus'
enum_types['RemoteStopTransactionResponse'] = dict()
enum_types['RemoteStopTransactionResponse']['status'] = 'RemoteStartStopStatus'
enum_types['ReserveNowResponse'] = dict()
enum_types['ReserveNowResponse']['status'] = 'ReservationStatus'
enum_types['ResetRequest'] = dict()
enum_types['ResetRequest']['type'] = 'ResetType'
enum_types['ResetResponse'] = dict()
enum_types['ResetResponse']['status'] = 'ResetStatus'
enum_types['SendLocalListRequest'] = dict()
enum_types['SendLocalListRequest']['updateType'] = 'UpdateType'
enum_types['SendLocalListResponse'] = dict()
enum_types['SendLocalListResponse']['status'] = 'UpdateStatus'
enum_types['CsChargingProfiles'] = dict()
enum_types['CsChargingProfiles']['chargingProfilePurpose'] = 'ChargingProfilePurposeType'
enum_types['CsChargingProfiles']['chargingProfileKind'] = 'ChargingProfileKindType'
enum_types['CsChargingProfiles']['recurrencyKind'] = 'RecurrencyKindType'
enum_types['SetChargingProfileRequest'] = dict()
enum_types['SetChargingProfileRequest']['CsChargingProfiles'] = 'ChargingProfile'
enum_types['SetChargingProfileResponse'] = dict()
enum_types['SetChargingProfileResponse']['status'] = 'ChargingProfileStatus'
enum_types['StatusNotificationRequest'] = dict()
enum_types['StatusNotificationRequest']['errorCode'] = 'ChargePointErrorCode'
enum_types['StatusNotificationRequest']['status'] = 'ChargePointStatus'
enum_types['StopTransactionRequest'] = dict()
enum_types['StopTransactionRequest']['reason'] = 'Reason'
enum_types['TriggerMessageRequest'] = dict()
enum_types['TriggerMessageRequest']['requestedMessage'] = 'MessageTrigger'
enum_types['TriggerMessageResponse'] = dict()
enum_types['TriggerMessageResponse']['status'] = 'TriggerMessageStatus'
enum_types['UnlockConnectorResponse'] = dict()
enum_types['UnlockConnectorResponse']['status'] = 'UnlockStatus'
enum_types['ExtendedTriggerMessageRequest'] = dict()
enum_types['ExtendedTriggerMessageRequest']['requestedMessage'] = 'MessageTriggerEnumType'
enum_types['ExtendedTriggerMessageResponse'] = dict()
enum_types['ExtendedTriggerMessageResponse']['status'] = 'TriggerMessageStatusEnumType'
# messages from OCPP 2.1
v21_messages = ['AFRRSignal',
'AdjustPeriodicEventStream',
'BatterySwap',
'ChangeTransactionTariff',
'ClearDERControl',
'ClearTariffs',
'ClosePeriodicEventStream',
'GetCertificateChainStatus',
'GetCRL',
'GetDERControl',
'GetPeriodicEventStream',
'GetTariffs',
'NotifyAllowedEnergyTransfer',
'NotifyDERAlarm',
'NotifyDERStartStop',
'NotifyPeriodicEventStream',
'NotifyPriorityCharging',
'NotifySettlement',
'NotifyWebPaymentStarted',
'OpenPeriodicEventStream',
'PullDynamicScheduleUpdate',
'ReportDERControl',
'RequestBatterySwap',
'SetDERControl',
'SetDefaultTariff',
'UpdateDynamicSchedule',
'UsePriorityCharging',
'VatNumberValidation',
]
send_messages = ['NotifyPeriodicEventStream']
def object_exists(name: str) -> bool:
"""Check if an object (i.e. dataclass) already exists."""
for el in parsed_types:
if el['name'] == name:
return True
return False
def add_enum_type(name: str, enums: Tuple[str]):
"""Add special class for enum types."""
for el in parsed_enums:
if el['name'] == name:
print(f'Warning: enum {name} already exists')
return
print("Adding enum type: %s" % name)
add = True
for el in parsed_enums_unique:
if el['name'] == name and el['enums'] == enums:
print("non-unique but same enum detected: %s:%s" % (name, enums))
add = False
if add:
parsed_enums.append({
'name': name,
'enums': enums
})
parsed_enums_unique.append({
'name': name,
'enums': enums
})
def parse_property(prop_name: str, prop: Dict, depends_on: List[str], ob_name=None) -> Tuple[str, bool]:
"""Determine type of property and proceed with it.
In case it is a $ref, look it up in the current_defs dict and parse
it again.
Currently, the following property types are supported:
- string (and enum as a special case)
- integer
- number
- boolean
- array
- object (will be parsed recursively)
"""
prop_type = None
is_enum = False
if 'type' not in prop:
if '$ref' in prop:
prop_type = prop['$ref'].split('/')[-1]
if prop_type not in current_defs:
raise Exception('$ref: ' + prop['$ref'] + ' not defined!')
def_prop = current_defs[prop_type]
prop_type = def_prop.get('javaType', prop_type)
return parse_property(prop_type, def_prop, depends_on)
else:
# if not defined, propably any
print(f"{prop_name} has no type defined")
return ('json', False)
if prop['type'] == 'string':
if 'enum' in prop:
prop_type = stringcase.capitalcase(prop_name)
if ob_name is not None and ob_name in enum_types:
print("%s changing type of enum %s from %s to '%s'" %
(ob_name, prop_name, prop_type, enum_types[ob_name][prop_name]))
prop_type = enum_types[ob_name][prop_name]
print("obname: %s, prop_type: %s" % (ob_name, prop_type))
add_enum_type(prop_type, prop['enum'])
is_enum = True
else:
if 'maxLength' in prop:
prop_type = 'CiString<{}>'.format(prop['maxLength'])
if ob_name == 'Get15118EVCertificateResponse' and prop_name == 'exiResponse':
prop_type = 'CiString<ISO15118_GET_EV_CERTIFICATE_EXI_RESPONSE_SIZE>'
else:
if 'format' in prop:
if prop['format'] in format_types:
prop_type = format_types[prop['format']]
else:
prop_type = 'TODO: std::string with format: {}'.format(
prop['format'])
else:
prop_type = 'std::string'
elif prop['type'] == 'integer':
prop_type = 'std::int32_t'
elif prop['type'] == 'number':
prop_type = 'float'
elif prop['type'] == 'boolean':
prop_type = 'bool'
elif prop['type'] == 'array':
prop_type = 'std::vector<' + \
parse_property(prop_name, prop['items'], depends_on)[0] + '>'
elif prop['type'] == 'object':
prop_type = stringcase.capitalcase(prop_name)
print("!!! prop_type: %s" % prop_type)
if prop_type == 'ConfigurationKey':
prop_type = 'KeyValue'
print("Changed prop type from ConfigurationKey to KeyValue according to spec")
if prop_type == 'CsChargingProfiles':
prop_type = 'ChargingProfile'
print(
"Changed prop type from CsChargingProfiles to ChargingProfile according to spec")
if prop_type == 'CertificateHashData':
prop_type = 'CertificateHashDataType'
print(
"Changed prop type from CertificateHashData to CertificateHashDataType according to spec")
depends_on.append(prop_type)
if not object_exists(prop_type):
parse_object(prop_type, prop)
else:
raise Exception('Unknown type: ' + prop['type'])
return (prop_type, is_enum)
def parse_object(ob_name: str, json_schema: Dict):
"""Parse a JSON schema object.
Iterates over the properties of this object, parses their type
and puts these information into the global dict parsed_types.
"""
ob_dict = {'name': ob_name, 'properties': [], 'depends_on': []}
parsed_types.insert(0, ob_dict)
if not 'properties' in json_schema:
return
for prop_name, prop in json_schema['properties'].items():
if not prop_name.isidentifier() or keyword.iskeyword(prop_name):
raise Exception(prop_name + ' can\'t be used as an identifier!')
prop_type, is_enum = parse_property(
prop_name, prop, ob_dict['depends_on'], ob_name)
if not is_enum:
is_enum = 'enum' in prop
for parsed_enum in parsed_enums:
if parsed_enum['name'] == prop_type:
is_enum = True
break
ob_dict['properties'].append({
'name': prop_name,
'json_name': prop_name,
'type': prop_type,
'enum': is_enum,
'required': prop_name in json_schema.get('required', {})
})
ob_dict['properties'].sort(key=lambda x: x.get('required'), reverse=True)
def parse_schemas(version: str, schema_dir: Path = Path('schemas/json/'),
generated_dir: Path = Path('generated/')):
"""Main entry for parsing OCPP json schema files.
Looks up the corresponding Request/Response JSON schema files for
each action. Parses each schema, generates out the corresponding
python source code, containing the objects in the schema as
dataclasses, and generate validation scripts.
"""
schemas = dict()
schema_files = list(schema_dir.glob('*.json'))
for schema_file in sorted(schema_files):
print(f"Schema file: {schema_file}")
with open(schema_file, 'r', encoding='utf-8-sig') as schema_dump:
schema = json.load(schema_dump)
stripped_fn = schema_file.stem
action = ''
request = False
send = False
if stripped_fn.endswith('Request'):
request = True
action = stripped_fn[0:-7]
elif stripped_fn.endswith('Response'):
action = stripped_fn[0:-8]
elif Path(schema_dir, stripped_fn + 'Response.json').exists():
request = True
action = stripped_fn
else:
# check if this is one of the SEND messages that do not have a Response
if stripped_fn in send_messages:
print(f'{stripped_fn} is one of the new OCPP 2.1 SEND messages')
# not a Request or Response but a SEND message
send = True
action = stripped_fn
else:
raise Exception('Invalid schema file', schema_file)
if request:
schemas.setdefault(action, {}).update({'req': schema})
else:
if send:
schemas.setdefault(action, {}).update({'send': schema})
else:
schemas.setdefault(action, {}).update({'res': schema})
# check if for every action, the request and response exists
for key, value in schemas.items():
if 'req' not in value or 'res' not in value:
if 'send' in value:
print('Message is a SEND message')
continue
raise Exception(
'Either response or request is missing for action: ' + key, value)
generated_header_dir = generated_dir / 'include' / 'ocpp' / version
generated_source_dir = generated_dir / 'lib' / 'ocpp' / version
generated_header_dir_v21 = generated_header_dir
generated_source_dir_v21 = generated_source_dir
if version == 'v2':
generated_header_dir_v21 = generated_dir / 'include' / 'ocpp' / 'v21'
generated_source_dir_v21 = generated_dir / 'lib' / 'ocpp' / 'v21'
if not generated_header_dir.exists():
generated_header_dir.mkdir(parents=True)
if not generated_source_dir.exists():
generated_source_dir.mkdir(parents=True)
if not generated_header_dir_v21.exists():
generated_header_dir_v21.mkdir(parents=True)
if not generated_source_dir_v21.exists():
generated_source_dir_v21.mkdir(parents=True)
enums_hpp_fn = Path(generated_header_dir, 'ocpp_enums.hpp')
enums_cpp_fn = Path(generated_source_dir, 'ocpp_enums.cpp')
ocpp_types_hpp_fn = Path(generated_header_dir, 'ocpp_types.hpp')
ocpp_types_cpp_fn = Path(generated_source_dir, 'ocpp_types.cpp')
messages_header_dir = generated_header_dir / 'messages'
messages_source_dir = generated_source_dir / 'messages'
messages_cmakelists_txt_fn = Path(messages_source_dir, 'CMakeLists.txt')
messages_header_dir_v21 = generated_header_dir_v21 / 'messages'
messages_source_dir_v21 = generated_source_dir_v21 / 'messages'
messages_cmakelists_txt_fn_v21 = Path(messages_source_dir_v21, 'CMakeLists.txt')
message_files = []
message_files_v21 = []
first = True
for action, type_of_action in schemas.items():
if action in v21_messages:
message_files_v21.append(action)
else:
message_files.append(action)
if not messages_header_dir.exists():
messages_header_dir.mkdir(parents=True)
if not messages_source_dir.exists():
messages_source_dir.mkdir(parents=True)
if not messages_header_dir_v21.exists():
messages_header_dir_v21.mkdir(parents=True)
if not messages_source_dir_v21.exists():
messages_source_dir_v21.mkdir(parents=True)
writemode = dict()
writemode['req'] = 'w'
writemode['res'] = 'a+'
writemode['send'] = 'w'
message_uses_optional = False
message_needs_enums = False
message_needs_types = False
message_needs_constants = False
for (type_name, type_key) in (('Request', 'req'), ('Response', 'res'), ('', 'send')):
if not type_key in type_of_action:
print(f'{type_key} not available for {type_of_action}')
continue
parsed_types.clear()
parsed_enums.clear()
# FIXME!
current_defs.clear()
current_defs.update(
type_of_action[type_key].get('definitions', {}))
action_class_name: str = action + type_name
root_schema = type_of_action[type_key]
parse_object(action_class_name, root_schema)
for e in parsed_enums:
parsed_enums_unique.remove(e)
# sort types, so no foward declaration is necessary
sorted_types: List = []
for class_type in parsed_types:
insert_at: int = 0
for dep_class_type in class_type['depends_on']:
for i, _type in enumerate(sorted_types):
# the new one depends on the current
if _type['name'] == dep_class_type:
insert_at = max(insert_at, i + 1)
break
sorted_types.insert(insert_at, class_type)
if not message_uses_optional:
message_uses_optional = uses_optional(sorted_types)
if not message_needs_enums:
message_needs_enums = needs_enums(sorted_types)
if not message_needs_types:
message_needs_types = needs_types(sorted_types)
if action == 'Get15118EVCertificate':
message_needs_constants = True
for (type_name, type_key) in (('Request', 'req'), ('Response', 'res'), ('', 'send')):
if not type_key in type_of_action:
print(f'type key {type_key} not available, skipping.')
continue
parsed_types.clear()
parsed_enums.clear()
# FIXME!
current_defs.clear()
current_defs.update(
type_of_action[type_key].get('definitions', {}))
action_class_name: str = action + type_name
root_schema = type_of_action[type_key]
parse_object(action_class_name, root_schema)
# sort types, so no foward declaration is necessary
sorted_types: List = []
for class_type in parsed_types:
insert_at: int = 0
for dep_class_type in class_type['depends_on']:
for i, _type in enumerate(sorted_types):
# the new one depends on the current
if _type['name'] == dep_class_type:
insert_at = max(insert_at, i + 1)
break
sorted_types.insert(insert_at, class_type)
message_version = version
generated_class_hpp_fn = Path(messages_header_dir, action + '.hpp')
generated_class_cpp_fn = Path(messages_source_dir, action + '.cpp')
conversions_namespace_prefix = ''
if action in v21_messages:
print(f'"{action}" is a OCPP 2.1 message')
message_version = 'v21'
generated_class_hpp_fn = Path(messages_header_dir_v21, action + '.hpp')
generated_class_cpp_fn = Path(messages_source_dir_v21, action + '.cpp')
conversions_namespace_prefix = 'ocpp::v2::'
if action == 'NotifyMonitoringReport':
# Exception needed for this specific message since the eventNotificationType
# field was marked as required in OCPP 2.1 which breaks schema validation
# for OCPP 2.0.1
for i, property in enumerate(sorted_types):
if property['name'] == 'VariableMonitoring':
for j, field in enumerate(property['properties']):
if field['name'] == 'eventNotificationType':
sorted_types[i]['properties'][j]['required'] = False
with open(generated_class_hpp_fn, writemode[type_key]) as out:
out.write(message_hpp_template.render({
'types': sorted_types,
'namespace': message_version,
'enum_types': parsed_enums,
'uses_optional': message_uses_optional,
'needs_enums': message_needs_enums,
'needs_types': message_needs_types,
'needs_constants': message_needs_constants,
'action': {
'name': action,
'class_name': action_class_name,
'type_key': type_key,
'is_request': (type_name == 'Request'),
'is_send': (type_key == 'send')
}
}))
with open(generated_class_cpp_fn, writemode[type_key]) as out:
out.write(message_cpp_template.render({
'types': sorted_types,
'namespace': message_version,
'enum_types': parsed_enums,
'uses_optional': message_uses_optional,
'needs_enums': message_needs_enums,
'needs_types': message_needs_types,
'conversions_namespace_prefix': conversions_namespace_prefix,
'action': {
'name': action,
'class_name': action_class_name,
'type_key': type_key,
'is_request': (type_name == 'Request'),
'is_send': (type_key == 'send')
}
}))
with open(enums_hpp_fn, 'w' if first else 'a+') as out:
out.write(enums_hpp_template.render({
'enum_types': parsed_enums,
'namespace': version_path,
'first': first,
'action': {
'name': action,
'class_name': action_class_name,
}
}))
with open(enums_cpp_fn, 'w' if first else 'a+') as out:
out.write(enums_cpp_template.render({
'enum_types': parsed_enums,
'namespace': version_path,
'first': first,
'action': {
'name': action,
'class_name': action_class_name,
}
}))
parsed_types_ = []
for parsed_type in sorted_types:
if parsed_type not in parsed_types_unique:
parsed_types_unique.append(parsed_type)
if parsed_type['name'] not in unique_types:
parsed_types_.append(parsed_type)
unique_types.add(parsed_type['name'])
with open(ocpp_types_hpp_fn, 'w' if first else 'a+') as out:
out.write(ocpp_types_hpp_template.render({
'parsed_types': parsed_types_,
'namespace': version_path,
'first': first,
'action': {
'name': action,
'class_name': action_class_name,
}
}))
with open(ocpp_types_cpp_fn, 'w' if first else 'a+') as out:
out.write(ocpp_types_cpp_template.render({
'parsed_types': parsed_types_,
'namespace': version_path,
'first': first,
'action': {
'name': action,
'class_name': action_class_name,
}
}))
first = False
if version == 'v2':
with open(messages_cmakelists_txt_fn_v21, 'w') as out:
out.write(messages_cmakelists_txt_template.render({
'messages': sorted(message_files_v21)
}))
with open(messages_cmakelists_txt_fn, 'w') as out:
out.write(messages_cmakelists_txt_template.render({
'messages': sorted(message_files)
}))
with open(enums_hpp_fn, 'a+') as out:
out.write(enums_hpp_template.render({
'last': True,
'namespace': version_path
}))
with open(enums_cpp_fn, 'a+') as out:
out.write(enums_cpp_template.render({
'last': True,
'namespace': version_path
}))
with open(ocpp_types_hpp_fn, 'a+') as out:
out.write(ocpp_types_hpp_template.render({
'last': True,
'namespace': version_path
}))
with open(ocpp_types_cpp_fn, 'a+') as out:
out.write(ocpp_types_cpp_template.render({
'last': True,
'namespace': version_path
}))
# clang-format generated files
subprocess.run(["sh", "-c", "find {} -regex '.*\\.\\(cpp\\|hpp\\)' -exec clang-format -style=file -i {{}} \\;".format(
messages_header_dir)], cwd=messages_header_dir)
subprocess.run(["sh", "-c", "find {} -regex '.*\\.\\(cpp\\|hpp\\)' -exec clang-format -style=file -i {{}} \\;".format(
messages_source_dir)], cwd=messages_source_dir)
subprocess.run(["sh", "-c", "find {} -regex '.*\\.\\(cpp\\|hpp\\)' -exec clang-format -style=file -i {{}} \\;".format(
messages_header_dir_v21)], cwd=messages_header_dir_v21)
subprocess.run(["sh", "-c", "find {} -regex '.*\\.\\(cpp\\|hpp\\)' -exec clang-format -style=file -i {{}} \\;".format(
messages_source_dir_v21)], cwd=messages_source_dir_v21)
subprocess.run(["clang-format", "-style=file", "-i",
enums_hpp_fn, ocpp_types_hpp_fn], cwd=generated_header_dir)
subprocess.run(["clang-format", "-style=file", "-i",
enums_cpp_fn, ocpp_types_cpp_fn], cwd=generated_source_dir)
subprocess.run(["clang-format", "-style=file", "-i",
enums_cpp_fn], cwd=generated_source_dir)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter, description="OCPP code generator")
parser.add_argument("--schemas", metavar='SCHEMAS',
help="Directory in which the OCPP 1.6 schemas reside", required=True)
parser.add_argument("--out", metavar='OUT',
help="Dir in which the generated code will be put", required=True)
parser.add_argument("--version", metavar='VERSION',
help="Version of OCPP [1.6, 2.0.1, or 2.1]", required=True)
args = parser.parse_args()
version = args.version
if version == '1.6' or version == '16' or version == 'v16':
version_path = 'v16'
elif version == '2.0.1' or version == '2' or version == '2.1' or version == '21' or \
version == '201' or version == 'v201' or version == 'v2' or version == 'v21':
version_path = 'v2'
else:
raise ValueError(f"Version {version} not a valid ocpp version")
schema_dir = Path(args.schemas).resolve()
generated_dir = Path(args.out).resolve()
parse_schemas(version=version_path, schema_dir=schema_dir,
generated_dir=generated_dir)

View File

@@ -0,0 +1,351 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#
"""
This script can be used to generate EVerest yaml type definitions based on OCPP2.0.1 or OCPP2.1 JSON schema files
"""
import re
import html
from typing import List, Optional, Dict
from jinja2 import Environment, FileSystemLoader, select_autoescape
from pathlib import Path
from dataclasses import dataclass
import json
import argparse
from utils import snake_case
@dataclass
class ArrayItem:
type: str
ref: Optional[str]
min_items: Optional[int]
max_items: Optional[int]
@dataclass
class Property:
name: str
description: str
type: str
ref: Optional[str]
item: Optional[ArrayItem]
required: bool
@dataclass
class DataType:
name: str
description: str
properties: List[Property]
@dataclass
class Enum:
name: str
description: str
values: List[str]
def __eq__(self, value):
return self.name == value.name
def setup_jinja_template():
"""
Sets up and returns the jinja template for everest_type.yaml.jinja
"""
relative_tmpl_path = Path(__file__).resolve().parent / "templates"
env = Environment(
loader=FileSystemLoader(relative_tmpl_path),
trim_blocks=True,
lstrip_blocks=True,
autoescape=select_autoescape(enabled_extensions=("html")),
)
env.filters["snake_case"] = snake_case
return env.get_template("everest_type.yaml.jinja")
def format_yaml_description(description, indent_level=6):
"""
Format the given description to render well in yaml
"""
if description is not None:
description = description.replace("+\r\n", "\n")
# Remove bold and underscores for formatting
description = re.sub(r"\*\*?([^*]+)\*\*?", r"\1", description)
# Remove paragraphs starting with "NOTE:"
description = re.sub(r"(?m)^NOTE:.*(?:\r?\n)?", "", description)
# Decode HTML entities
description = html.unescape(description)
# Remove references like <<...,...>>
description = re.sub(r"<<[^>]+>>", "", description)
# Clean up extra spaces and newlines
description = re.sub(r"\s*\r?\n\s*", " ", description).strip()
# Reformat multiline descriptions with proper indentation
wrapped_lines = re.sub(r"(.{1,80})(?:\s|$)", r"\1\n", description).splitlines()
indent = " " * indent_level
formatted_description = "\n".join(
f"{indent}{line.strip()}" for line in wrapped_lines
).strip()
return formatted_description
return description
def get_data_type_dependencies(data_type: DataType) -> List[str]:
"""
Gets a list of all definitions referenced by the given data_type
"""
dependencies = [_property.ref for _property in data_type.properties if _property.ref]
for _property in data_type.properties:
if _property.ref:
dependencies.append(_property.ref)
elif _property.item and _property.item.ref:
dependencies.append(_property.item.ref)
return dependencies
def sort_data_types(data_types: List[DataType]) -> List[DataType]:
"""
Sorts the given data_types so that dependencies of types are listed before
they are used as part of a property of a data_type. This spares forward declaration
of types.
"""
sorted_types: List = []
for data_type in data_types:
insert_at: int = 0
depends_on = get_data_type_dependencies(data_type)
for dep_type in depends_on:
for i, _type in enumerate(sorted_types):
# the new one depends on the current
if _type.name == dep_type:
insert_at = max(insert_at, i + 1)
break
sorted_types.insert(insert_at, data_type)
return sorted_types
def parse_enum(definition_type: Dict) -> Enum:
"""
Parses Enum of given definition_type.
"""
values = [
value.replace(".", "_").replace("-", "_") for value in definition_type["enum"]
]
description = format_yaml_description(definition_type.get("description"))
return Enum(definition_type.get("javaType"), description, values)
def parse_item(_property: Dict):
"""
Parses ArrayItem of the given property
"""
property_item_type = _property.get("items").get("type", "object")
property_item_ref = None
if "$ref" in _property.get("items"):
if _property.get("items").get("$ref").endswith("EnumType"):
property_item_type = "string"
property_item_ref = (
_property.get("items").get("$ref").split("/")[-1].replace("Type", "")
)
min_items = _property.get("minItems")
max_items = _property.get("maxItems")
return ArrayItem(property_item_type, property_item_ref, min_items, max_items)
def parse_property(
_property: Dict, property_name: str, definition_type: Dict, definitions: List[Dict]
):
"""
Parses Property from the given arguments
"""
property_type = _property.get("type", "object")
property_ref = None
if "$ref" in _property:
property_ref = _property["$ref"].split("/")[-1].replace("Type", "")
property_required = property_name in definition_type.get("required", [])
property_description = None
if "description" in _property:
property_description = _property.get("description")
elif property_ref is not None:
property_description = definitions.get(f"{property_ref}Type").get("description")
property_item = None
if property_type == "array":
property_item = parse_item(_property)
if property_description is None and property_item.ref is not None:
property_description = definitions.get(f"{property_item.ref}Type").get(
"description"
)
property_description = format_yaml_description(
property_description, indent_level=10
)
return Property(
property_name,
property_description,
property_type,
property_ref,
property_item,
property_required,
)
def parse_type(definition_type: Dict, definitions) -> DataType:
"""
Parses DataType of given definition_type
"""
properties = []
for property_name, _property in definition_type.get("properties").items():
properties.append(
parse_property(_property, property_name, definition_type, definitions)
)
data_type_description = format_yaml_description(definition_type.get("description"))
return DataType(definition_type.get("javaType"), data_type_description, properties)
def parse_types_and_enums(schema_dir: Path, types_to_generate: List[str]):
"""
Parses Types and Enums based on the given schema_dir and types_to_generate
"""
enums = []
types = []
schema_files = list(schema_dir.glob("*.json"))
for schema_file in sorted(schema_files):
with open(schema_file, "r", encoding="utf-8-sig") as schema_dump:
schema = json.load(schema_dump)
definitions = schema.get("definitions")
if definitions is None:
continue
for definition_name, definition_type in definitions.items():
if "enum" in definition_type and definition_name in types_to_generate:
enum = parse_enum(definition_type)
if enum not in enums:
enums.append(enum)
elif (
"enum" not in definition_type
and definition_name in types_to_generate
):
_type = parse_type(definition_type, definitions)
if _type not in types:
types.append(_type)
types = sort_data_types(types)
return types, enums
def load_schemas_from_directory(schema_dir) -> Dict:
_schema_files = {}
schema_files = list(schema_dir.glob("*.json"))
for schema_file in sorted(schema_files):
with open(schema_file, "r", encoding="utf-8-sig") as schema_dump:
schema = json.load(schema_dump)
if "definitions" in schema:
_schema_files[schema_file] = schema["definitions"]
return _schema_files
def resolve_references(definition, to_process, resolved_types):
if isinstance(definition, dict):
for key, value in definition.items():
if key == "$ref":
ref_type = value.split("/")[-1]
if ref_type not in resolved_types:
to_process.add(ref_type)
else:
resolve_references(value, to_process, resolved_types)
elif isinstance(definition, list):
for item in definition:
resolve_references(item, to_process, resolved_types)
def get_required_types(schema_dir, target_types):
schema_files = load_schemas_from_directory(schema_dir)
resolved_types = set()
to_process = set(target_types)
type_to_file = {}
for file_name, definitions in schema_files.items():
for type_name in definitions.keys():
type_to_file[type_name] = file_name
while to_process:
current = to_process.pop()
if current in resolved_types:
continue
resolved_types.add(current)
file_name = type_to_file.get(current)
if not file_name:
raise ValueError(f"Type {current} not found in any schema file.")
definition = schema_files[file_name].get(current, {})
resolve_references(definition, to_process, resolved_types)
return resolved_types
def generate_everest_types(
schema_dir: Path, out_file: Path, types_to_generate: List[str]
):
types, enums = parse_types_and_enums(schema_dir, types_to_generate)
with open(out_file, "w") as out:
out.write(
setup_jinja_template().render(
{
"enums": enums,
"types": types,
}
)
)
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
description="Code generator for EVerest types from OCPP JSON schemas",
)
parser.add_argument(
"--schemas",
metavar="SCHEMAS",
help="Directory in which the OCPP schemas reside",
required=True,
)
parser.add_argument(
"--out",
metavar="OUT",
help="Dir in which the generated EVerest YAML types will be put",
required=True,
)
parser.add_argument(
"--types",
metavar="OUT",
help="Specify comma seperated list of OCPP Types that should be generated in YAML",
required=False,
)
args = parser.parse_args()
schema_dir = Path(args.schemas).resolve()
out_file = Path(args.out).resolve()
types_to_generate = []
if args.types:
types_to_generate = args.types.split(",")
types_to_generate = get_required_types(schema_dir, types_to_generate)
generate_everest_types(schema_dir, out_file, types_to_generate)

View File

@@ -0,0 +1,51 @@
description: Autogenerated types based on OCPP JSON schemas
types:
{% for enum in enums %}
{{ enum.name }}:
description: >-
{{ enum.description }}
type: string
enum:
{% for value in enum.values %}
- "{{ value }}"
{% endfor %}
{% endfor %}
{% for type in types %}
{{ type.name }}:
description: >-
{{ type.description }}
type: object
required:
{% if type.properties|selectattr('required', 'true')|list %}
{% for property in type.properties if property.required %}
- {{ property.name | snake_case }}
{% endfor %}
{% else %}
[]
{% endif %}
properties:
{% for property in type.properties %}
{% if property.name != "customData" %}
{{ property.name | snake_case }}:
description: >-
{{ property.description }}
type: {{ property.type }}
{% if property.type == "object" or property.enum %}
$ref: /ocpp#/{{ property.ref }}
{% endif %}
{% if property.type == "array" %}
items:
type: {{ property.item.type }}
{% if property.item.ref != None %}
$ref: /ocpp#/{{ property.item.ref }}
{% endif %}
{% if property.item.min_items != None %}
minItems: {{ property.item.min_items }}
{% endif %}
{% if property.item.max_items != None %}
maxItems: {{ property.item.max_items }}
{% endif %}
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -0,0 +1,186 @@
{% if action.is_request or action.is_send %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#include <ocpp/{{namespace}}/messages/{{action.name}}.hpp>
#include <ostream>
#include <string>
{% if uses_optional %}
#include <optional>
{% endif %}
using json = nlohmann::json;
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{% for type in types %}
{% if type.name == action.class_name %}
{% if loop.last %}
std::string {{ type.name }}::get_type() const {
return "{{ type.name | remove_last('Request') }}";
}
void to_json(json& j, const {{ type.name }}& k) {
// the required parts of the message
{% if type.properties|selectattr('required')|list|length %}
j = json{
{%- endif %}
{%- for property in type.properties %}
{%- if property.required %}{"{{property.name}}",
{%- if property.enum %}
{{ conversions_namespace_prefix }}conversions::{{- property.type | snake_case}}_to_string(k.{{property.name}})
{%- else %}
{%- if property.type == 'ocpp::DateTime' %}
k.{{property.name}}.to_rfc3339()
{% elif property.type.startswith('std::vector<') and property.type.endswith('Enum>') %}
json::array()
{%- else %}
k.{{property.name}}
{%- endif %}
{%- endif %}},
{%- endif %}
{%- endfor %}
{% if not type.properties|selectattr('required')|list|length %}
j = json ({}, true);
{%- else %}
};
{%- endif %}
{%- for property in type.properties %}
{%- if property.required and property.type.startswith('std::vector<') and property.type.endswith('Enum>')%}
for (const auto& val : k.{{property.name}}) {
{%- if property.type.endswith('Enum>') %}
j["{{property.name}}"].push_back({{ conversions_namespace_prefix }}conversions::{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}_to_string(val));
{%- else%}
j["{{property.name}}"].push_back(val);
{%- endif%}
}
{%- endif%}
{%- endfor %}
// the optional parts of the message
{% for property in type.properties %}
{% if not property.required %}
if (k.{{property.name}}) {
{% if property.type.startswith('std::vector<') %}
{%- if type.properties|selectattr('required')|list|length %}
j["{{property.name}}"] = json::array();
{%- else %}
{#only optional keys in json#}
{#TODO: add key to json when there are no required keys but multiple optional keys#}
if (j.empty()) {
j = json{{'{{"'+property.name+'", json::array()}};'}}
} else {
j["{{property.name}}"] = json::array();
}
{% endif %}
for (const auto& val : k.{{property.name}}.value()) {
{%- if property.type.endswith('Enum>') %}
j["{{property.name}}"].push_back({{ conversions_namespace_prefix }}conversions::{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}_to_string(val));
{%- else%}
j["{{property.name}}"].push_back(val);
{%- endif%}
}
{% else %}
{% if property.enum %}
j["{{property.name}}"] = {{ conversions_namespace_prefix }}conversions::{{- property.type | snake_case}}_to_string(k.{{property.name}}.value());
{% else %}
{% if property.type == 'ocpp::DateTime' %}
j["{{property.name}}"] = k.{{property.name}}.value().to_rfc3339();
{% else %}
j["{{property.name}}"] = k.{{property.name}}.value();
{% endif %}
{% endif %}
{% endif %}
}
{% endif %}
{% endfor %}
{% if not type.properties|length %}
(void)k; // no elements to unpack, silence unused parameter warning
{% endif %}
}
void from_json(const json& j, {{ type.name }}& k) {
// the required parts of the message
{% for property in type.properties %}
{% if property.required %}
{% if property.type.startswith('std::vector<') %}
for (const auto& val : j.at("{{property.name}}")) {
{%- if property.type.endswith('Enum>') %}
k.{{property.name}}.push_back({{ conversions_namespace_prefix }}conversions::string_to_{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}(val));
{% else %}
k.{{property.name}}.push_back(val);
{% endif %}
}
{% else %}
k.{{property.name}} =
{%- if property.enum %}
{{ conversions_namespace_prefix }}conversions::string_to_{{- property.type | snake_case}}(j.at("{{property.name}}"))
{%- else %}
{%- if property.type == 'ocpp::DateTime' %}
ocpp::DateTime(std::string(j.at("{{property.name}}")))
{%- else %}
j.at("{{property.name}}")
{%- endif %}
{%- endif %};
{% endif %}
{% endif %}
{%- endfor %}
// the optional parts of the message
{% for property in type.properties %}
{% if not property.required %}
if (j.contains("{{property.name}}")) {
{% if property.type.startswith('std::vector<') %}
const json& arr = j.at("{{property.name}}");
{{property.type}} vec;
for (const auto& val : arr) {
{%- if property.type.endswith('Enum>') %}
vec.push_back({{ conversions_namespace_prefix }}conversions::string_to_{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}(val));
{%- else %}
vec.push_back(val);
{%- endif %}
}
k.{{property.name}}.emplace(vec);
{% else %}
{% if property.enum %}
k.{{property.name}}.emplace({{ conversions_namespace_prefix }}conversions::string_to_{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}(j.at("{{property.name}}")));
{% else %}
{% if property.type == 'ocpp::DateTime' %}
k.{{property.name}}.emplace(ocpp::DateTime(std::string(j.at("{{property.name}}"))));
{% else %}
k.{{property.name}}.emplace(j.at("{{property.name}}"));
{% endif %}
{% endif %}
{% endif %}
}
{% endif %}
{% endfor %}
{% if not type.properties|length %}
// no elements to unpack, silence unused parameter warning
(void)j;
(void)k;
{% endif %}
}
/// \brief Writes the string representation of the given {{ type.name }} \p k to the given output stream \p os
/// \returns an output stream with the {{ type.name }} written to
std::ostream& operator<<(std::ostream& os, const {{ type.name }}& k) {
os << json(k).dump(4);
return os;
}
{% endif %}
{% endif %}
{% endfor %}
{% if not action.is_request or action.is_send %}
} // namespace {{namespace}}
} // namespace ocpp
{% endif %}

View File

@@ -0,0 +1,76 @@
{% if action.is_request or action.is_send %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#ifndef OCPP_{{namespace | upper}}_{{ action.name|upper }}_HPP
#define OCPP_{{namespace | upper}}_{{ action.name|upper }}_HPP
{% if uses_optional %}
#include <optional>
{% endif %}
#include <nlohmann/json_fwd.hpp>
{% if needs_constants %}
#include <ocpp/{{namespace}}/constants.hpp>
{% endif %}
{% if needs_enums %}
{% if namespace == 'v21' %}
#include <ocpp/v2/ocpp_enums.hpp>
{% else %}
#include <ocpp/{{namespace}}/ocpp_enums.hpp>
{% endif %}
{% endif %}
{% if namespace == 'v21' %}
#include <ocpp/v2/ocpp_types.hpp>
using namespace ocpp::v2;
{% else %}
#include <ocpp/{{namespace}}/ocpp_types.hpp>
{% endif %}
{% if needs_types %}
#include <ocpp/common/types.hpp>
{% endif %}
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{% for type in types %}
{% if type.name == action.class_name %}
/// \brief Contains a OCPP {{ type.name | replace('Request', '') }} message
struct {{ type.name }} : public ocpp::Message {
{% for property in type.properties %}
{{ 'std::optional<' if not property.required -}}
{{ property.type -}}
{{ '>' if not property.required -}}
{{ ' ' + property.name + ';' }}
{% endfor %}
{% if loop.last %}
/// \brief Provides the type of this {{ type.name | replace('Request', '') }} message as a human readable string
/// \returns the message type as a human readable string
std::string get_type() const override;
};
/// \brief Conversion from a given {{ type.name }} \p k to a given json object \p j
void to_json(json& j, const {{ type.name }}& k);
/// \brief Conversion from a given json object \p j to a given {{ type.name }} \p k
void from_json(const json& j, {{ type.name }}& k);
/// \brief Writes the string representation of the given {{ type.name }} \p k to the given output stream \p os
/// \returns an output stream with the {{ type.name }} written to
std::ostream& operator<<(std::ostream& os, const {{ type.name }}& k);
{% endif %}
{% endif %}
{% endfor %}
{% if not action.is_request or action.is_send %}
} // namespace {{namespace}}
} // namespace ocpp
#endif // OCPP_{{namespace | upper}}_{{ action.name|upper }}_HPP
{% endif %}

View File

@@ -0,0 +1,8 @@
target_sources(ocpp
PRIVATE
{% for message in messages %}
{{message}}.cpp
{% endfor %}
)

View File

@@ -0,0 +1,52 @@
{% if first %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#include <ocpp/{{namespace}}/ocpp_enums.hpp>
#include <string>
#include <ocpp/common/types.hpp>
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{%- if enum_types|length %}
{%- for enum_type in enum_types %}
// from: {{action.class_name}}
namespace conversions {
std::string {{ enum_type.name | snake_case }}_to_string({{ enum_type.name }} e) {
switch (e) {
{% for enum in enum_type.enums %}
case {{ enum_type.name }}::{{ enum.replace('.', '_').replace('-', '_') }}: return "{{enum}}";
{% endfor %}
}
throw EnumToStringException{e, "{{ enum_type.name }}"};
}
{{ enum_type.name }} string_to_{{ enum_type.name | snake_case }}(const std::string& s) {
{% for enum in enum_type.enums %}
if(s == "{{enum}}") {
return {{ enum_type.name }}::{{ enum.replace('.', '_').replace('-', '_') }};
}
{% endfor %}
throw StringToEnumException{s, "{{ enum_type.name }}"};
}
}
std::ostream& operator<<(std::ostream& os, const {{ enum_type.name }}& {{ enum_type.name | snake_case }}) {
os << conversions::{{ enum_type.name | snake_case }}_to_string({{ enum_type.name | snake_case }});
return os;
}
{% endfor %}
{%- endif %}
{% if last %}
} // namespace {{namespace}}
} // namespace ocpp
{% endif %}

View File

@@ -0,0 +1,47 @@
{% if first %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#ifndef OCPP_{{namespace | upper}}_OCPP_ENUMS_HPP
#define OCPP_{{namespace | upper}}_OCPP_ENUMS_HPP
#include <iosfwd>
#include <string>
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{%- if enum_types|length %}
{%- for enum_type in enum_types %}
// from: {{action.class_name}}
enum class {{ enum_type.name }}
{
{% for enum in enum_type.enums %}
{{ enum.replace('.', '_').replace('-', '_') }},
{% endfor %}
};
namespace conversions {
/// \brief Converts the given {{ enum_type.name }} \p e to human readable string
/// \returns a string representation of the {{ enum_type.name }}
std::string {{ enum_type.name | snake_case }}_to_string({{ enum_type.name }} e);
/// \brief Converts the given std::string \p s to {{ enum_type.name }}
/// \returns a {{ enum_type.name }} from a string representation
{{ enum_type.name }} string_to_{{ enum_type.name | snake_case }}(const std::string& s);
}
/// \brief Writes the string representation of the given {{ enum_type.name }} \p {{ enum_type.name | snake_case }} to the given output stream \p os
/// \returns an output stream with the {{ enum_type.name }} written to
std::ostream& operator<<(std::ostream& os, const {{ enum_type.name }}& {{ enum_type.name | snake_case }});
{% endfor %}
{%- endif %}
{% if last %}
} // namespace {{namespace}}
} // namespace ocpp
#endif // OCPP_{{namespace | upper}}_OCPP_ENUMS_HPP
{% endif %}

View File

@@ -0,0 +1,177 @@
{% if first %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#include <ocpp/{{namespace}}/ocpp_types.hpp>
#include <string>
#include <optional>
#include <nlohmann/json.hpp>
#include <ocpp/{{namespace}}/ocpp_enums.hpp>
#include <ocpp/common/types.hpp>
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{%- if parsed_types|length %}
{%- for type in parsed_types %}
{% if not type.name.endswith('Request') and not type.name.endswith('Response') and not (type.name == "CustomData" and namespace == "v2") %}
/// \brief Conversion from a given {{ type.name }} \p k to a given json object \p j
void to_json(json& j, const {{ type.name }}& k) {
// the required parts of the message
{% if type.properties|selectattr('required')|list|length %}
j = json{
{%- endif %}
{%- for property in type.properties %}
{%- if property.required %}{"{{property.name}}",
{%- if property.enum %}
conversions::{{- property.type | snake_case}}_to_string(k.{{property.name}})
{%- else %}
{%- if property.type == 'ocpp::DateTime' %}
k.{{property.name}}.to_rfc3339()
{%- else %}
k.{{property.name}}
{%- endif %}
{%- endif %}},
{%- endif %}
{%- endfor %}
{% if not type.properties|selectattr('required')|list|length %}
j = json ({}, true);
{%- else %}
};
{%- endif %}
// the optional parts of the message
{% for property in type.properties %}
{% if not property.required %}
if (k.{{property.name}}) {
{% if property.type.startswith('std::vector<') %}
{%- if type.properties|selectattr('required')|list|length %}
j["{{property.name}}"] = json::array();
{%- else %}
{#only optional keys in json#}
{#TODO: add key to json when there are no required keys but multiple optional keys#}
if (j.empty()) {
j = json{{'{{"'+property.name+'", json::array()}};'}}
} else {
j["{{property.name}}"] = json::array();
}
{% endif %}
for (const auto& val : k.{{property.name}}.value()) {
{%- if property.type.endswith('Enum>') %}
j["{{property.name}}"].push_back(conversions::{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}_to_string(val));
{%- else%}
j["{{property.name}}"].push_back(val);
{%- endif%}
}
{% else %}
{%- if property.enum %}
j["{{property.name}}"] = conversions::{{- property.type | snake_case}}_to_string(k.{{property.name}}.value());
{%- else %}
{%- if property.type == 'ocpp::DateTime' %}
j["{{property.name}}"] = k.{{property.name}}.value().to_rfc3339();
{%- else %}
j["{{property.name}}"] = k.{{property.name}}.value();
{%- endif %}
{%- endif %}
{% endif %}
}
{% endif %}
{% endfor %}
}
/// \brief Conversion from a given json object \p j to a given {{ type.name }} \p k
void from_json(const json& j, {{ type.name }}& k) {
// the required parts of the message
{% for property in type.properties %}
{% if property.required %}
{% if property.type.startswith('std::vector<') %}
for (const auto& val : j.at("{{property.name}}")) {
k.{{property.name}}.push_back(val);
}
{% else %}
k.{{property.name}} =
{%- if property.enum %}
conversions::string_to_{{- property.type | snake_case}}(j.at("{{property.name}}"))
{%- else %}
{%- if property.type == 'ocpp::DateTime' %}
ocpp::DateTime(std::string(j.at("{{property.name}}")))
{%- else %}
j.at("{{property.name}}")
{%- endif %}
{%- endif %};
{% endif %}
{% endif %}
{%- endfor %}
// the optional parts of the message
{% for property in type.properties %}
{% if not property.required %}
if (j.contains("{{property.name}}")) {
{% if property.type.startswith('std::vector<') %}
const json& arr = j.at("{{property.name}}");
{{property.type}} vec;
for (const auto& val : arr) {
{%- if property.type.endswith('Enum>') %}
vec.push_back(conversions::string_to_{{- property.type.replace('std::vector<','').replace('>','') | snake_case}}(val));
{%- else %}
vec.push_back(val);
{%- endif %}
}
k.{{property.name}}.emplace(vec);
{% else %}
{% if property.enum %}
k.{{property.name}}.emplace(conversions::string_to_{{- property.type | snake_case}}(j.at("{{property.name}}")));
{% else %}
{%- if property.type == 'ocpp::DateTime' %}
k.{{property.name}}.emplace(j.at("{{property.name}}").get<std::string>());
{%- else %}
{% if type.name == "VariableAttribute" and property.name == "value"%}
const json& value = j.at("value");
if (value.is_string()) {
k.value = value;
} else if (value.is_boolean()) {
if (value.get<bool>()) {
// Convert to lower case if that is not the case currently.
k.value = "true";
} else {
// Convert to lower case if that is not the case currently.
k.value = "false";
}
} else if (value.is_array() || value.is_object()) {
// Maybe this is correct and is just a string (json value string), so just return the string.
k.value = value.dump();
} else {
k.value = value.dump();
}
{% else %}
k.{{property.name}}.emplace(j.at("{{property.name}}"));
{% endif %}
{%- endif %}
{% endif %}
{% endif %}
}
{% endif %}
{% endfor %}
}
// \brief Writes the string representation of the given {{ type.name }} \p k to the given output stream \p os
/// \returns an output stream with the {{ type.name }} written to
std::ostream& operator<<(std::ostream& os, const {{ type.name }}& k) {
os << json(k).dump(4);
return os;
}
{% endif %}
{% endfor %}
{%- endif %}
{% if last %}
} // namespace {{namespace}}
} // namespace ocpp
{% endif %}

View File

@@ -0,0 +1,56 @@
{% if first %}
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - {{year}} Pionix GmbH and Contributors to EVerest
// This code is generated using the generator in 'src/code_generator/common`, please do not edit manually
#ifndef OCPP_{{namespace | upper}}_OCPP_TYPES_HPP
#define OCPP_{{namespace | upper}}_OCPP_TYPES_HPP
#include <string>
#include <optional>
#include <nlohmann/json_fwd.hpp>
#include <ocpp/{{namespace}}/ocpp_enums.hpp>
#include <ocpp/common/types.hpp>
namespace ocpp {
namespace {{namespace}} {
{% endif %}
{%- if parsed_types|length %}
{%- for type in parsed_types %}
{% if not type.name.endswith('Request') and not type.name.endswith('Response') %}
{% if type.name == "CustomData" and namespace == "v2" %}
using CustomData = nlohmann::json;
{% else %}
struct {{type.name}} {
{% for property in type.properties %}
{{ 'std::optional<' if not property.required -}}
{{ property.type -}}
{{ '>' if not property.required -}}
{{ ' ' + property.name + ';' }}
{% endfor %}
};
{% endif %}
{% if not (type.name == "CustomData" and namespace == "v2") %}
/// \brief Conversion from a given {{ type.name }} \p k to a given json object \p j
void to_json(json& j, const {{ type.name }}& k);
/// \brief Conversion from a given json object \p j to a given {{ type.name }} \p k
void from_json(const json& j, {{ type.name }}& k);
/// \brief Writes the string representation of the given {{ type.name }} \p k to the given output stream \p os
/// \returns an output stream with the {{ type.name }} written to
std::ostream& operator<<(std::ostream& os, const {{ type.name }}& k);
{% endif %}
{% endif %}
{% endfor %}
{%- endif %}
{% if last %}
} // namespace {{namespace}}
} // namespace ocpp
#endif // OCPP_{{namespace | upper}}_OCPP_TYPES_HPP
{% endif %}

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env python3
#
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#
def snake_case(word: str) -> str:
"""Convert capital case to snake case.
Only alphanumerical characters are allowed. Only inserts camelcase
between a consecutive lower and upper alphabetical character and
lowers first letter.
"""
# special handling for soc
word = word.replace('SoC', 'Soc')
out = ''
if len(word) == 0:
return out
cur_char: str = ''
for i, character in enumerate(word):
if i == 0:
cur_char = character
if not cur_char.isalnum():
raise Exception('Non legal character in: ' + word)
out += cur_char.lower()
continue
last_char: str = cur_char
cur_char = character
if (last_char.islower() and last_char.isalpha() and
cur_char.isupper() and cur_char.isalpha):
out += '_'
if not cur_char.isalnum():
out += '_'
else:
out += cur_char.lower()
return out