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:
17
tools/EVerest-main/lib/everest/ocpp/src/CMakeLists.txt
Normal file
17
tools/EVerest-main/lib/everest/ocpp/src/CMakeLists.txt
Normal 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)
|
||||
321
tools/EVerest-main/lib/everest/ocpp/src/charge_point.cpp
Normal file
321
tools/EVerest-main/lib/everest/ocpp/src/charge_point.cpp
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
```
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
target_sources(ocpp
|
||||
PRIVATE
|
||||
{% for message in messages %}
|
||||
{{message}}.cpp
|
||||
{% endfor %}
|
||||
)
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user