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:
138
tools/EVerest-main/applications/utils/.clang-format
Normal file
138
tools/EVerest-main/applications/utils/.clang-format
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveMacros: true
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentCaseLabels: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
Standard: Latest
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
...
|
||||
|
||||
43
tools/EVerest-main/applications/utils/.eslintrc.json
Normal file
43
tools/EVerest-main/applications/utils/.eslintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"objects": "always-multiline",
|
||||
"arrays": "always-multiline",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"import/no-unresolved": [
|
||||
2,
|
||||
{
|
||||
"ignore": [
|
||||
"everestjs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
3
tools/EVerest-main/applications/utils/.gitignore
vendored
Normal file
3
tools/EVerest-main/applications/utils/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
**/__pycache__
|
||||
workspace.yaml
|
||||
/bazel-*
|
||||
28
tools/EVerest-main/applications/utils/BUILD.bazel
Normal file
28
tools/EVerest-main/applications/utils/BUILD.bazel
Normal file
@@ -0,0 +1,28 @@
|
||||
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
|
||||
load("@bazel_skylib//rules:write_file.bzl", "write_file")
|
||||
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
write_file(
|
||||
name = "gen_update",
|
||||
out = "update.sh",
|
||||
content = [
|
||||
"#!/usr/bin/env bash",
|
||||
"cd $BUILD_WORKSPACE_DIRECTORY",
|
||||
"cp -fv bazel-everest-core/external/rules_python++pip+everest-testing_pip_deps/requirements.bzl applications/utils/requirements.bzl",
|
||||
],
|
||||
)
|
||||
|
||||
sh_binary(
|
||||
name = "vendor_requirements",
|
||||
srcs = ["update.sh"],
|
||||
data = ["@everest-testing_pip_deps//:requirements.bzl"],
|
||||
)
|
||||
|
||||
diff_test(
|
||||
name = "vendored_requirements_diff",
|
||||
failure_message = "Please run `bazel run //:vendor_requirements` to update the vendored requirements.bzl file.",
|
||||
file1 = "@everest-testing_pip_deps//:requirements.bzl",
|
||||
file2 = ":requirements.bzl",
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
)
|
||||
25
tools/EVerest-main/applications/utils/CMakeLists.txt
Normal file
25
tools/EVerest-main/applications/utils/CMakeLists.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
project(everest-utils
|
||||
VERSION 0.7.3
|
||||
DESCRIPTION "A collection of utilities for the EVerest project"
|
||||
LANGUAGES CXX C
|
||||
)
|
||||
|
||||
find_package(everest-cmake 0.5 REQUIRED
|
||||
PATHS ../everest-cmake
|
||||
)
|
||||
|
||||
ev_setup_cmake_variables_python_wheel()
|
||||
|
||||
ev_add_pip_package(
|
||||
NAME ev-dev-tools
|
||||
SOURCE_DIRECTORY ev-dev-tools
|
||||
)
|
||||
|
||||
ev_add_pip_package(
|
||||
NAME everest-testing
|
||||
SOURCE_DIRECTORY everest-testing
|
||||
)
|
||||
|
||||
add_subdirectory(everest-testing)
|
||||
201
tools/EVerest-main/applications/utils/LICENSE
Normal file
201
tools/EVerest-main/applications/utils/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
tools/EVerest-main/applications/utils/THIRD_PARTY.md
Normal file
1
tools/EVerest-main/applications/utils/THIRD_PARTY.md
Normal file
@@ -0,0 +1 @@
|
||||
_Use this file to list out any third-party dependencies used by this project. You may choose to point to a Gemfile or other language specific packaging file for details._
|
||||
3
tools/EVerest-main/applications/utils/docker/everest-docker-image/.gitignore
vendored
Normal file
3
tools/EVerest-main/applications/utils/docker/everest-docker-image/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.tar.gz
|
||||
user_config.yaml
|
||||
ocpp_user_config.json
|
||||
@@ -0,0 +1,118 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:12-slim AS builder
|
||||
|
||||
ARG REPO
|
||||
ARG BRANCH
|
||||
ARG EVEREST_CONFIG
|
||||
ARG OCPP_CONFIG
|
||||
ARG ADDITIONAL_CMAKE_PARAMETERS
|
||||
ARG INSTALL_EV_CLI
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
git \
|
||||
rsync \
|
||||
wget \
|
||||
cmake \
|
||||
doxygen \
|
||||
graphviz \
|
||||
build-essential \
|
||||
clang-format \
|
||||
clang-tidy \
|
||||
cppcheck \
|
||||
libboost-all-dev \
|
||||
maven \
|
||||
openjdk-17-jdk \
|
||||
nodejs \
|
||||
npm \
|
||||
libsqlite3-dev \
|
||||
python3-pip \
|
||||
libssl-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libpcap-dev \
|
||||
libcap-dev \
|
||||
libsystemd-dev \
|
||||
python3-venv \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /workspace/everest
|
||||
|
||||
# to avoid caching
|
||||
ARG BUILD_DATE=Unknown
|
||||
|
||||
# add github to known hosts
|
||||
RUN mkdir -p -m 0600 ~/.ssh
|
||||
RUN ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
|
||||
RUN mkdir -p /workspace/everest/cpm_source_cache
|
||||
ENV CPM_SOURCE_CACHE="/workspace/everest/cpm_source_cache"
|
||||
ENV EVEREST_VENV=/workspace/everest/venv
|
||||
RUN python3 -m venv ${EVEREST_VENV}
|
||||
ENV PATH="${EVEREST_VENV}/bin:${PATH}"
|
||||
RUN git clone https://github.com/EVerest/everest-cmake.git
|
||||
RUN git clone https://github.com/EVerest/EVerest.git -b "${BRANCH}"
|
||||
WORKDIR /workspace/everest/EVerest/applications/utils/ev-dev-tools
|
||||
RUN if [ "${INSTALL_EV_CLI}" = "install_ev_cli" ] ; then python3 -m pip install . ; fi
|
||||
WORKDIR /workspace/everest
|
||||
RUN git clone https://github.com/EVerest/ext-switchev-iso15118.git
|
||||
WORKDIR /workspace/everest/ext-switchev-iso15118/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
WORKDIR /workspace/everest
|
||||
|
||||
RUN rm -rf "/workspace/everest/$(basename "${REPO}" .git)"
|
||||
RUN --mount=type=ssh git clone ${REPO} -b "${BRANCH}"
|
||||
|
||||
RUN --mount=type=ssh rm -rf "/workspace/everest/$(basename "${REPO}" .git)/build" && \
|
||||
cd "/workspace/everest/$(basename "${REPO}" .git)" && \
|
||||
git checkout "${BRANCH}" && \
|
||||
mkdir "/workspace/everest/$(basename "${REPO}" .git)/build" && \
|
||||
cd "/workspace/everest/$(basename "${REPO}" .git)/build" && \
|
||||
cmake .. -DCMAKE_INSTALL_PREFIX=/opt/everest ${ADDITIONAL_CMAKE_PARAMETERS} && \
|
||||
make -j"$(nproc)" install
|
||||
|
||||
# Try to copy the OCPP 2.0.1 config directory to have the init_device_model_db.py script available for (re-)initialization of the device model
|
||||
RUN cp -R "$(grep -m 1 "ocpp_SOURCE_DIR:STATIC=" "/workspace/everest/$(basename "${REPO}" .git)/build/CMakeCache.txt" | sed "s/ocpp_SOURCE_DIR:STATIC=//")/config/v201" /opt/everest/ocpp201config || echo "Could not copy OCPP 2.0.1 config directory"
|
||||
RUN mkdir -p /opt/everest/config/user-config
|
||||
COPY "${EVEREST_CONFIG}" /opt/everest/config/
|
||||
COPY "${OCPP_CONFIG}" /opt/everest/config/
|
||||
RUN if [ "${EVEREST_CONFIG}" != "config.yaml" ]; then mv /opt/everest/config/"${EVEREST_CONFIG}" /opt/everest/config/config.yaml ; fi
|
||||
RUN if [ "${OCPP_CONFIG}" != "ocpp-config.json" ]; then mv /opt/everest/config/"${OCPP_CONFIG}" /opt/everest/config/ocpp-config.json ; fi
|
||||
|
||||
COPY logging.ini /opt/everest/config
|
||||
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM debian:12-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
openjdk-17-jre \
|
||||
nodejs \
|
||||
npm \
|
||||
curl \
|
||||
python3-pip \
|
||||
sqlite3 \
|
||||
libboost-program-options1.74.0 \
|
||||
libboost-log1.74.0 \
|
||||
libboost-chrono1.74.0 \
|
||||
libboost-system1.74.0 \
|
||||
libssl3 \
|
||||
libcurl4 \
|
||||
libcap2 \
|
||||
less \
|
||||
libevent-dev \
|
||||
python3-venv \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV EVEREST_VENV=/workspace/everest/venv
|
||||
RUN python3 -m venv ${EVEREST_VENV}
|
||||
ENV PATH="${EVEREST_VENV}/bin:${PATH}"
|
||||
COPY --from=builder /workspace/everest/venv /workspace/everest/venv
|
||||
|
||||
WORKDIR /opt/everest
|
||||
COPY --from=builder /opt/everest ./
|
||||
COPY ./scripts/initialize.sh /opt/everest
|
||||
|
||||
CMD [ "/opt/everest/bin/manager", "--conf", "/opt/everest/config/config.yaml" ]
|
||||
@@ -0,0 +1,45 @@
|
||||
# EVerest with docker
|
||||
|
||||
You can build docker images of EVerest or out-of-tree repositories using the provided utility script.
|
||||
|
||||
## Build an EVerest docker image
|
||||
|
||||
With
|
||||
|
||||
```bash
|
||||
./build.sh [--conf <string>]
|
||||
```
|
||||
|
||||
a docker image of EVerest will be created using the given EVerest configuration file.
|
||||
|
||||
**It is important to note that if you require ssh access, the ssh agent forwarding should be done without sudo privileges.
|
||||
By running the script in sudo this changes the user and causes issues with the ssh forwarding.**
|
||||
It is possible to make your user not require sudo privileges when running docker, this should be done instead.
|
||||
|
||||
Specify the following options to create your desired docker image of EVerest:
|
||||
|
||||
* repo: Git repository (e.g. https://github.com/EVerest/EVerest.git) - Optional, defaults to: https://github.com/EVerest/EVerest.git
|
||||
* branch: Git branch or tag name (e.g main or 2024.6.0) - Optional, defaults to: main
|
||||
* conf: Path to EVerest config file (e.g. /home/$(whoami)/checkout/everest-workspace/EVerest/config/ config-sil.yaml) - Required.
|
||||
* ocpp-conf: Path to EVerest OCPP config file (e.g. /home/$(whoami)/checkout/everest-workspace/libocpp/aux/config-docker.json) - Optional, defaults to: ocpp-config.json
|
||||
* name: Name of the docker image (e.g everest) - Optional, defaults to: everest
|
||||
* build-date: Build date of the docker image, is reflected in its name and can have an effect on caching - Optional, defaults to the current datetime
|
||||
|
||||
```bash
|
||||
./build.sh [--repo <GIT-REPOSITORY>] [--branch <BRANCH-NAME>] [--conf <EVEREST-CONFIG>] [--ocpp-conf <OCPP-CONFIG>] [--name <IMAGE-NAME>] [--build-date 2042]
|
||||
```
|
||||
Remember to provide an ocpp configuration file if you use an EVerest config that loads the OCPP module. Be aware that the provided OCPP config file will always be named `ocpp-config.json` inside the docker container. Consider this when configuring the OCPP module within the EVerest config and to set the ChargePointConfigPath accordingly.
|
||||
|
||||
Images will be created in a .tar.gz format in this folder.
|
||||
|
||||
## Run EVerest in docker
|
||||
|
||||
To run the image in a docker container, run the following commands
|
||||
|
||||
```bash
|
||||
docker load < <YOUR_IMAGE-TIMESTAMP.tar.gz>
|
||||
docker run --rm -it --network host <IMAGE_NAME>
|
||||
```
|
||||
|
||||
## Run EVerest with OCPP 2.0.1 in docker
|
||||
To run EVerest with OCPP 2.0.1 you can use the provided docker-compose.yaml with the example run-config-fallback.sh script
|
||||
@@ -0,0 +1,5 @@
|
||||
active_modules:
|
||||
system:
|
||||
module: System
|
||||
|
||||
x-module-layout: {}
|
||||
@@ -0,0 +1,819 @@
|
||||
[
|
||||
{
|
||||
"name": "ChargingStatusIndicator",
|
||||
"variables": {
|
||||
"ChargingStatusIndicatorActive": {
|
||||
"variable_name": "Active",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
},
|
||||
"ChargingStatusIndicatorColor": {
|
||||
"variable_name": "Color",
|
||||
"attributes": {
|
||||
"Actual": "FFFF00"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Connector",
|
||||
"evse_id": 1,
|
||||
"connector_id": 1,
|
||||
"variables": {
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EVSE",
|
||||
"evse_id": 1,
|
||||
"variables": {
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Connector",
|
||||
"evse_id": 2,
|
||||
"connector_id": 1,
|
||||
"variables": {
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EVSE",
|
||||
"evse_id": 2,
|
||||
"variables": {
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SampledDataCtrlr",
|
||||
"variables": {
|
||||
"SampledDataCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"SampledDataTxEndedInterval": {
|
||||
"variable_name": "TxEndedInterval",
|
||||
"attributes": {
|
||||
"Actual": "60"
|
||||
}
|
||||
},
|
||||
"SampledDataTxEndedMeasurands": {
|
||||
"variable_name": "TxEndedMeasurands",
|
||||
"attributes": {
|
||||
"Actual": "Energy.Active.Import.Register,Current.Import"
|
||||
}
|
||||
},
|
||||
"SampledDataTxStartedMeasurands": {
|
||||
"variable_name": "TxStartedMeasurands",
|
||||
"attributes": {
|
||||
"Actual": "Energy.Active.Import.Register,Current.Import"
|
||||
}
|
||||
},
|
||||
"SampledDataTxUpdatedInterval": {
|
||||
"variable_name": "TxUpdatedInterval",
|
||||
"attributes": {
|
||||
"Actual": "120"
|
||||
}
|
||||
},
|
||||
"SampledDataTxUpdatedMeasurands": {
|
||||
"variable_name": "TxUpdatedMeasurands",
|
||||
"attributes": {
|
||||
"Actual": "Energy.Active.Import.Register,Current.Import,Voltage,Power.Active.Import,Power.Reactive.Import,Frequency"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "LocalAuthListCtrlr",
|
||||
"variables": {
|
||||
"BytesPerMessageSendLocalList": {
|
||||
"variable_name": "BytesPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"LocalAuthListCtrlrEntries": {
|
||||
"variable_name": "Entries",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"ItemsPerMessageSendLocalList": {
|
||||
"variable_name": "ItemsPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"LocalAuthListCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "InternalCtrlr",
|
||||
"variables": {
|
||||
"ChargePointId": {
|
||||
"variable_name": "ChargePointId",
|
||||
"attributes": {
|
||||
"Actual": "cp001"
|
||||
}
|
||||
},
|
||||
"NetworkConnectionProfiles": {
|
||||
"variable_name": "NetworkConnectionProfiles",
|
||||
"attributes": {
|
||||
"Actual": "[{\"configurationSlot\": 1, \"connectionData\": {\"messageTimeout\": 30, \"ocppCsmsUrl\": \"ws://localhost:9000/cp001\", \"ocppInterface\": \"Wired0\", \"ocppTransport\": \"JSON\", \"ocppVersion\": \"OCPP20\", \"securityProfile\": 1}}]"
|
||||
}
|
||||
},
|
||||
"ChargeBoxSerialNumber": {
|
||||
"variable_name": "ChargeBoxSerialNumber",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"ChargePointModel": {
|
||||
"variable_name": "ChargePointModel",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"ChargePointVendor": {
|
||||
"variable_name": "ChargePointVendor",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"FirmwareVersion": {
|
||||
"variable_name": "FirmwareVersion",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"SupportedCiphers12": {
|
||||
"variable_name": "SupportedCiphers12",
|
||||
"attributes": {
|
||||
"Actual": "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384"
|
||||
}
|
||||
},
|
||||
"SupportedCiphers13": {
|
||||
"variable_name": "SupportedCiphers13",
|
||||
"attributes": {
|
||||
"Actual": "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"
|
||||
}
|
||||
},
|
||||
"NumberOfConnectors": {
|
||||
"variable_name": "NumberOfConnectors",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"LogMessagesFormat": {
|
||||
"variable_name": "LogMessagesFormat",
|
||||
"attributes": {
|
||||
"Actual": "log,html"
|
||||
}
|
||||
},
|
||||
"SupportedCriteria": {
|
||||
"variable_name": "SupportedCriteria",
|
||||
"attributes": {
|
||||
"Actual": "Enabled,Active,Available,Problem"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "OCPPCommCtrlr",
|
||||
"variables": {
|
||||
"FileTransferProtocols": {
|
||||
"variable_name": "FileTransferProtocols",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"MessageTimeout": {
|
||||
"variable_name": "MessageTimeout",
|
||||
"attributes": {
|
||||
"Actual": "60"
|
||||
},
|
||||
"instance": "Default"
|
||||
},
|
||||
"MessageAttemptInterval": {
|
||||
"variable_name": "MessageAttemptInterval",
|
||||
"attributes": {
|
||||
"Actual": "10"
|
||||
},
|
||||
"instance": "TransactionEvent"
|
||||
},
|
||||
"MessageAttempts": {
|
||||
"variable_name": "MessageAttempts",
|
||||
"attributes": {
|
||||
"Actual": "5"
|
||||
},
|
||||
"instance": "TransactionEvent"
|
||||
},
|
||||
"NetworkConfigurationPriority": {
|
||||
"variable_name": "NetworkConfigurationPriority",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"NetworkProfileConnectionAttempts": {
|
||||
"variable_name": "NetworkProfileConnectionAttempts",
|
||||
"attributes": {
|
||||
"Actual": "3"
|
||||
}
|
||||
},
|
||||
"OfflineThreshold": {
|
||||
"variable_name": "OfflineThreshold",
|
||||
"attributes": {
|
||||
"Actual": "60"
|
||||
}
|
||||
},
|
||||
"ResetRetries": {
|
||||
"variable_name": "ResetRetries",
|
||||
"attributes": {
|
||||
"Actual": "3"
|
||||
}
|
||||
},
|
||||
"RetryBackOffRandomRange": {
|
||||
"variable_name": "RetryBackOffRandomRange",
|
||||
"attributes": {
|
||||
"Actual": "2"
|
||||
}
|
||||
},
|
||||
"RetryBackOffRepeatTimes": {
|
||||
"variable_name": "RetryBackOffRepeatTimes",
|
||||
"attributes": {
|
||||
"Actual": "2"
|
||||
}
|
||||
},
|
||||
"RetryBackOffWaitMinimum": {
|
||||
"variable_name": "RetryBackOffWaitMinimum",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"UnlockOnEVSideDisconnect": {
|
||||
"variable_name": "UnlockOnEVSideDisconnect",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"WebSocketPingInterval": {
|
||||
"variable_name": "WebSocketPingInterval",
|
||||
"attributes": {
|
||||
"Actual": "30"
|
||||
}
|
||||
},
|
||||
"OCPPCommCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "DisplayMessageCtrlr",
|
||||
"variables": {
|
||||
"NumberOfDisplayMessages": {
|
||||
"variable_name": "DisplayMessages",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"DisplayMessageSupportedFormats": {
|
||||
"variable_name": "SupportedFormats",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"DisplayMessageSupportedPriorities": {
|
||||
"variable_name": "SupportedPriorities",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"DisplayMessageCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ClockCtrlr",
|
||||
"variables": {
|
||||
"DateTime": {
|
||||
"variable_name": "DateTime",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"TimeSource": {
|
||||
"variable_name": "TimeSource",
|
||||
"attributes": {
|
||||
"Actual": "Heartbeat"
|
||||
}
|
||||
},
|
||||
"ClockCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ReservationCtrlr",
|
||||
"variables": {}
|
||||
},
|
||||
{
|
||||
"name": "ISO15118Ctrlr",
|
||||
"variables": {
|
||||
"ContractValidationOffline": {
|
||||
"variable_name": "ContractValidationOffline",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"ReservationCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TxCtrlr",
|
||||
"variables": {
|
||||
"EVConnectionTimeOut": {
|
||||
"variable_name": "EVConnectionTimeOut",
|
||||
"attributes": {
|
||||
"Actual": "120"
|
||||
}
|
||||
},
|
||||
"StopTxOnEVSideDisconnect": {
|
||||
"variable_name": "StopTxOnEVSideDisconnect",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"StopTxOnInvalidId": {
|
||||
"variable_name": "StopTxOnInvalidId",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"TxStartPoint": {
|
||||
"variable_name": "TxStartPoint",
|
||||
"attributes": {
|
||||
"Actual": "PowerPathClosed"
|
||||
}
|
||||
},
|
||||
"TxStopPoint": {
|
||||
"variable_name": "TxStopPoint",
|
||||
"attributes": {
|
||||
"Actual": "EVConnected,Authorized"
|
||||
}
|
||||
},
|
||||
"TxCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AlignedDataCtrlr",
|
||||
"variables": {
|
||||
"AlignedDataInterval": {
|
||||
"variable_name": "Interval",
|
||||
"attributes": {
|
||||
"Actual": "900"
|
||||
}
|
||||
},
|
||||
"AlignedDataMeasurands": {
|
||||
"variable_name": "Measurands",
|
||||
"attributes": {
|
||||
"Actual": "Energy.Active.Import.Register,Voltage,Frequency"
|
||||
}
|
||||
},
|
||||
"AlignedDataTxEndedInterval": {
|
||||
"variable_name": "TxEndedInterval",
|
||||
"attributes": {
|
||||
"Actual": "60"
|
||||
}
|
||||
},
|
||||
"AlignedDataTxEndedMeasurands": {
|
||||
"variable_name": "TxEndedMeasurands",
|
||||
"attributes": {
|
||||
"Actual": "Energy.Active.Import.Register,Voltage"
|
||||
}
|
||||
},
|
||||
"AlignedDataSendDuringIdle": {
|
||||
"variable_name": "SendDuringIdle",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
},
|
||||
"AlignedDataCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AuthCtrlr",
|
||||
"variables": {
|
||||
"AuthorizeRemoteStart": {
|
||||
"variable_name": "AuthorizeRemoteStart",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"LocalAuthorizeOffline": {
|
||||
"variable_name": "LocalAuthorizeOffline",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"LocalPreAuthorize": {
|
||||
"variable_name": "LocalPreAuthorize",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"MasterPassGroupId": {
|
||||
"variable_name": "MasterPassGroupId",
|
||||
"attributes": {
|
||||
"Actual": "123"
|
||||
}
|
||||
},
|
||||
"AuthCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "AuthCacheCtrlr",
|
||||
"variables": {
|
||||
"AuthCacheCtrlrAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"AuthCacheCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"AuthCacheStorage": {
|
||||
"variable_name": "Storage",
|
||||
"attributes": {
|
||||
"Actual": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ChargingStation",
|
||||
"variables": {
|
||||
"ChargingStationAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"ChargingStationAvailable": {
|
||||
"variable_name": "Available",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
},
|
||||
"ChargingStationSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"ChargingStationPhaseRotation": {
|
||||
"variable_name": "PhaseRotation",
|
||||
"attributes": {
|
||||
"Actual": "RST"
|
||||
}
|
||||
},
|
||||
"ChargingStationProblem": {
|
||||
"variable_name": "Problem",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "CustomizationCtrlr",
|
||||
"variables": {}
|
||||
},
|
||||
{
|
||||
"name": "DeviceDataCtrlr",
|
||||
"variables": {
|
||||
"BytesPerMessageGetReport": {
|
||||
"variable_name": "BytesPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 250
|
||||
},
|
||||
"instance": "GetReport"
|
||||
},
|
||||
"BytesPerMessageGetVariables": {
|
||||
"variable_name": "BytesPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 250
|
||||
},
|
||||
"instance": "GetVariables"
|
||||
},
|
||||
"BytesPerMessageSetVariables": {
|
||||
"variable_name": "BytesPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariables"
|
||||
},
|
||||
"ItemsPerMessageGetReport": {
|
||||
"variable_name": "ItemsPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 4
|
||||
},
|
||||
"instance": "GetReport"
|
||||
},
|
||||
"ItemsPerMessageGetVariables": {
|
||||
"variable_name": "ItemsPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 2
|
||||
},
|
||||
"instance": "GetVariables"
|
||||
},
|
||||
"ItemsPerMessageSetVariables": {
|
||||
"variable_name": "ItemsPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariables"
|
||||
},
|
||||
"ReportingValueSize": {
|
||||
"variable_name": "ReportingValueSize",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariables"
|
||||
},
|
||||
"ValueSize": {
|
||||
"variable_name": "ValueSize",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariables"
|
||||
},
|
||||
"DeviceDataCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "TariffCostCtrlr",
|
||||
"variables": {
|
||||
"TariffCostCtrlrCurrency": {
|
||||
"variable_name": "Currency",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"TariffFallbackMessage": {
|
||||
"variable_name": "TariffFallbackMessage",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"TotalCostFallbackMessage": {
|
||||
"variable_name": "TotalCostFallbackMessage",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"TariffCostCtrlrEnabledCost": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SecurityCtrlr",
|
||||
"variables": {
|
||||
"CertificateEntries": {
|
||||
"variable_name": "CertificateEntries",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"OrganizationName": {
|
||||
"variable_name": "OrganizationName",
|
||||
"attributes": {
|
||||
"Actual": "Pionix"
|
||||
}
|
||||
},
|
||||
"SecurityProfile": {
|
||||
"variable_name": "SecurityProfile",
|
||||
"attributes": {
|
||||
"Actual": "1"
|
||||
}
|
||||
},
|
||||
"Identity": {
|
||||
"variable_name": "Identity",
|
||||
"attributes": {
|
||||
"Actual": "cp001"
|
||||
}
|
||||
},
|
||||
"BasicAuthPassword": {
|
||||
"variable_name": "BasicAuthPassword",
|
||||
"attributes": {
|
||||
"Actual": "DEADBEEFDEADBEEF"
|
||||
}
|
||||
},
|
||||
"SecurityCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SmartChargingCtrlr",
|
||||
"variables": {
|
||||
"EntriesChargingProfiles": {
|
||||
"variable_name": "Entries",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "ChargingProfiles"
|
||||
},
|
||||
"LimitChangeSignificance": {
|
||||
"variable_name": "LimitChangeSignificance",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"PeriodsPerSchedule": {
|
||||
"variable_name": "PeriodsPerSchedule",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"ChargingProfileMaxStackLevel": {
|
||||
"variable_name": "ProfileStackLevel",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
}
|
||||
},
|
||||
"ChargingScheduleChargingRateUnit": {
|
||||
"variable_name": "RateUnit",
|
||||
"attributes": {
|
||||
"Actual": ""
|
||||
}
|
||||
},
|
||||
"SmartChargingCtrlrAvailableEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "MonitoringCtrlr",
|
||||
"variables": {
|
||||
"BytesPerMessageSetVariableMonitoring": {
|
||||
"variable_name": "BytesPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariableMonitoring"
|
||||
},
|
||||
"ItemsPerMessageSetVariableMonitoring": {
|
||||
"variable_name": "ItemsPerMessage",
|
||||
"attributes": {
|
||||
"Actual": 42
|
||||
},
|
||||
"instance": "SetVariableMonitoring"
|
||||
},
|
||||
"MonitoringCtrlrEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"attributes": {
|
||||
"Actual": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
services:
|
||||
mqtt-server:
|
||||
image: eclipse-mosquitto:2.0.18
|
||||
ports:
|
||||
- 1883:1883
|
||||
volumes:
|
||||
- ../mosquitto/mosquitto.conf:/mosquitto/config/mosquitto.conf
|
||||
nodered:
|
||||
build:
|
||||
context: ./nodered
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- 1880:1880
|
||||
volumes:
|
||||
- ./nodered/flows:/data/flows
|
||||
depends_on:
|
||||
- mqtt-server
|
||||
everest:
|
||||
image: everest:latest
|
||||
ports:
|
||||
- 8849:8849
|
||||
depends_on:
|
||||
- mqtt-server
|
||||
environment:
|
||||
- MQTT_SERVER_ADDRESS=mqtt-server
|
||||
sysctls:
|
||||
- net.ipv6.conf.all.disable_ipv6=0
|
||||
volumes:
|
||||
- ./logs:/opt/everest/logs
|
||||
- ./configuration/everest/${EVEREST_CONFIG}:/opt/everest/config/config.yaml
|
||||
- ./configuration/ocpp/${OCPP_CONFIG}:/opt/everest/config/ocpp-config.json
|
||||
command: ${EVEREST_COMMAND}
|
||||
@@ -0,0 +1,18 @@
|
||||
# for documentation on this file format see:
|
||||
# https://www.boost.org/doc/libs/1_54_0/libs/log/doc/html/log/detailed/utilities.html#log.detailed.utilities.setup.filter_formatter
|
||||
|
||||
[Core]
|
||||
DisableLogging=false
|
||||
Filter="%Severity% >= INFO"
|
||||
|
||||
[Sinks.Console]
|
||||
Destination=Console
|
||||
# Filter="%Target% contains \"MySink1\""
|
||||
Format="%TimeStamp% [%Severity%] \033[1;32m%Process%\033[0m \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%"
|
||||
Asynchronous=false
|
||||
AutoFlush=true
|
||||
SeverityStringColorDebug="\033[1;30m"
|
||||
SeverityStringColorInfo="\033[1;37m"
|
||||
SeverityStringColorWarning="\033[1;33m"
|
||||
SeverityStringColorError="\033[1;31m"
|
||||
SeverityStringColorCritical="\033[1;35m"
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM nodered/node-red:2.2.3
|
||||
RUN npm install node-red-dashboard
|
||||
RUN npm install node-red-contrib-ui-actions
|
||||
RUN npm install node-red-node-ui-table
|
||||
RUN npm install node-red-contrib-ui-level
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [--conf <string>] [--ocpp-conf <string>] [--init]" 1>&2
|
||||
echo -e "\t--conf: Path to EVerest config file - Optional, defaults to config-fallback.yaml"
|
||||
echo -e "\t--ocpp-conf: Path to EVerest OCPP config file - Optional, defaults to ocpp-config.json"
|
||||
echo -e "\t--init: If the OCPP 2.0.1 device model should be re-initialized with the given OCPP config - Optional"
|
||||
exit 1
|
||||
}
|
||||
|
||||
export EVEREST_CONFIG=config-fallback.yaml
|
||||
export OCPP_CONFIG=ocpp-config.json
|
||||
export EVEREST_COMMAND="sh -c '/opt/everest/bin/manager --conf /opt/everest/config/config.yaml'"
|
||||
|
||||
while [ ! -z "$1" ]; do
|
||||
if [ "$1" == "--conf" ]; then
|
||||
export EVEREST_CONFIG="${2}"
|
||||
shift 2
|
||||
elif [ "$1" == "--ocpp-conf" ]; then
|
||||
export OCPP_CONFIG="${2}"
|
||||
shift 2
|
||||
elif [ "$1" == "--init" ]; then
|
||||
export EVEREST_COMMAND="sh -c '/opt/everest/initialize.sh'"
|
||||
shift 1
|
||||
else
|
||||
usage
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
docker compose up
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "initialize.sh script used to re-initialize EVerest OCPP 2.0.1 device model and starting everest"
|
||||
|
||||
python3 /opt/everest/ocpp201config/init_device_model_db.py --db /opt/everest/share/everest/modules/OCPP201/device_model_storage.db --config /opt/everest/config/ocpp-config.json --schemas /opt/everest/ocpp201config/component_schemas/ init insert
|
||||
|
||||
/opt/everest/bin/manager --conf /opt/everest/config/config.yaml
|
||||
3
tools/EVerest-main/applications/utils/ev-dev-tools/.gitignore
vendored
Normal file
3
tools/EVerest-main/applications/utils/ev-dev-tools/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build
|
||||
__pycache__
|
||||
*.egg-info
|
||||
@@ -0,0 +1,31 @@
|
||||
load("//applications/utils:requirements.bzl", "requirement")
|
||||
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
exports_files(
|
||||
["BUILD.bazel"],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "lib",
|
||||
srcs = glob(["src/ev_cli/*.py"]),
|
||||
imports = ["src"],
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
deps = [
|
||||
requirement("jinja2"),
|
||||
requirement("jsonschema"),
|
||||
requirement("stringcase"),
|
||||
requirement("pyyaml"),
|
||||
],
|
||||
)
|
||||
|
||||
py_binary(
|
||||
name = "ev-cli",
|
||||
srcs = ["src/ev_cli/ev.py"],
|
||||
data = glob(["src/ev_cli/templates/**"]),
|
||||
legacy_create_init = False,
|
||||
main = "src/ev_cli/ev.py",
|
||||
visibility = ["//visibility:public"],
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
deps = [":lib"],
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
ev_create_python_wheel_targets(
|
||||
PACKAGE_NAME
|
||||
"ev-dev-tools"
|
||||
)
|
||||
201
tools/EVerest-main/applications/utils/ev-dev-tools/LICENSE
Normal file
201
tools/EVerest-main/applications/utils/ev-dev-tools/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
195
tools/EVerest-main/applications/utils/ev-dev-tools/README.rst
Normal file
195
tools/EVerest-main/applications/utils/ev-dev-tools/README.rst
Normal file
@@ -0,0 +1,195 @@
|
||||
=========================
|
||||
EVerest development tools
|
||||
=========================
|
||||
|
||||
This python project currently consists of the following packages
|
||||
|
||||
- `ev_cli`: EVerest module auto generation
|
||||
|
||||
Install
|
||||
-------
|
||||
To install `ev_cli`:
|
||||
|
||||
python3 -m pip install .
|
||||
|
||||
ev_cli
|
||||
------
|
||||
|
||||
The `ev_cli` package comes with a command line tool, named ``ev-cli``.
|
||||
It has the following subcommands
|
||||
|
||||
- module:
|
||||
auto generation and update of EVerest modules from its interface and
|
||||
manifest definitions
|
||||
|
||||
- interface:
|
||||
auto generation of c++ header files for defined interfaces
|
||||
|
||||
- helpers:
|
||||
utility commands
|
||||
|
||||
There exist short forms, for all subcommands and options. Simply call:
|
||||
|
||||
ev-cli --help
|
||||
|
||||
for getting the list of short forms.
|
||||
|
||||
Both the `module` and `interface` command have the following options in
|
||||
common:
|
||||
|
||||
- `--work-dir`:
|
||||
work directory which also contains the manifest definitions (default: ``.``)
|
||||
|
||||
- `--everest-dir`:
|
||||
root directory of EVerest core or any directory containing interface
|
||||
and module definitions (default: ``.``)
|
||||
|
||||
- `--schemas-dir`:
|
||||
schemas directory of the EVerest framework, containing the schema
|
||||
definitions (default: ``EVerest/lib/everest/framework/schemas``)
|
||||
|
||||
- `--clang-format-file`:
|
||||
if c++output should be formatted, set this to the path of the
|
||||
.clang-format file
|
||||
|
||||
Generating c++ header files for defined interfaces
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Assuming that the interface definitions in json format are located at
|
||||
``./interfaces/*.json``, simply:
|
||||
|
||||
ev-cli interfaces generate-headers
|
||||
|
||||
This will generate the c++ header files for all interfaces and output them
|
||||
to ``./generated/include/generated``. To generate only a single interface, call:
|
||||
|
||||
ev-cli interfaces generate-headers InterfaceName.json
|
||||
|
||||
For each interface an ``Implementation.hpp`` and ``Interface.hpp``
|
||||
header file will be generated. The former represents the `implementers`
|
||||
view, and the latter the `users` view of the interface, when used in a
|
||||
module.
|
||||
|
||||
Creating and updating auto generated files for modules (c++ only)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Assuming the modules are located at ``./modules`` and the initial
|
||||
skeleton for a module named `Example` with its manifest in
|
||||
``./modules/Example/manifest.json`` should be created, call:
|
||||
|
||||
ev-cli module create Example
|
||||
|
||||
This will create the following files inside the ``./modules/Example``
|
||||
subdirectory
|
||||
|
||||
- ``CMakeLists.txt``:
|
||||
build instruction file for CMake
|
||||
|
||||
- ``ld-ev.hpp``/``ld-ev.cpp``:
|
||||
glue code files for this module to get hooked up by the EVerest
|
||||
framework
|
||||
|
||||
- ``Example.hpp``/``Example.cpp``:
|
||||
header and source file for the module
|
||||
|
||||
Furthermore, for each interface provided by the module a subdirectory
|
||||
with the name of the `interface id` will be created. If, for example,
|
||||
the manifest looks like::
|
||||
|
||||
{
|
||||
"description": "Example module",
|
||||
"provides": {
|
||||
"main": {
|
||||
"description": "SampleInterface implementation",
|
||||
"interface": "SampleInterface",
|
||||
...
|
||||
}
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
a subdirectory named ``main`` will be created, including two files
|
||||
``SampleInterfaceImpl.hpp`` and ``SampleInterfaceImpl.cpp``. The header
|
||||
file declares the implementation of `SampleInterface`, which derives
|
||||
from the auto generated interface header files from the previous
|
||||
subsection.
|
||||
|
||||
Now it is up to the user to implement logic in the module and interface
|
||||
implementation `cpp` source files.
|
||||
|
||||
If the modules' ``manifest.json`` or inferface definitions, used by the
|
||||
module, change, you can update the generated files by using:
|
||||
|
||||
ev-cli module update Example
|
||||
|
||||
**Note**:
|
||||
|
||||
1.
|
||||
``cpp`` source files will never be changed or overwritten by the
|
||||
`update` subcommand. The `create` subcommand only resets / overrides
|
||||
the files when using the ``--force`` option
|
||||
|
||||
2.
|
||||
``hpp`` header files and the ``CMakeLists.txt`` file will get
|
||||
updated, if its interface dependencies definitions change and the
|
||||
`update` subcommand is used. You can force an update by using the
|
||||
``--force`` option. During an update, the sections marked like::
|
||||
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
.....
|
||||
// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1
|
||||
|
||||
will be kept. If you want to completely reset / override these
|
||||
files, you need to recreate the using `create` subcommand with the
|
||||
``--force`` option.
|
||||
|
||||
3.
|
||||
Generated files will never be deleted. So make sure, you do this if
|
||||
you, for example, change the interface ids or remove interfaces from
|
||||
the module
|
||||
|
||||
These additional options might be useful for the `create` and `update`
|
||||
subcommands:
|
||||
|
||||
1. ``--force``:
|
||||
force creation or update
|
||||
|
||||
2. ``--diff``:
|
||||
don't touch anything, only show a `diff` of what would be changed
|
||||
|
||||
3. ``--only``:
|
||||
this option takes a comma separated list of files, that should be
|
||||
touched only. This is especially helpful, if you want to recreate
|
||||
only a single interface implementation ``cpp`` file, because you
|
||||
changed the corresponding interface a lot. To get a list of possible files, you can simply call:
|
||||
|
||||
ev-cli module create Example --only which
|
||||
|
||||
this would output for the above mentioned example::
|
||||
|
||||
Available files for category "core"
|
||||
cmakelists
|
||||
ld-ev.hpp
|
||||
ld-ev.cpp
|
||||
module.hpp
|
||||
module.cpp
|
||||
Available files for category "interfaces"
|
||||
main.hpp
|
||||
main.cpp
|
||||
|
||||
So calling:
|
||||
|
||||
ev-cli module create Example --only main.cpp,cmakelists --force
|
||||
|
||||
would recreate the ``CMakeLists.txt`` and the
|
||||
``main/SampleInterfaceImpl.cpp`` files, whereas:
|
||||
|
||||
ev-clie module update Example --only module.hpp
|
||||
|
||||
would update only the module header file ``Example.hpp``
|
||||
|
||||
|
||||
Auto generating NodeJS modules
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
**tbd**
|
||||
@@ -0,0 +1,9 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.autopep8]
|
||||
max_line_length = 120
|
||||
34
tools/EVerest-main/applications/utils/ev-dev-tools/setup.cfg
Normal file
34
tools/EVerest-main/applications/utils/ev-dev-tools/setup.cfg
Normal file
@@ -0,0 +1,34 @@
|
||||
[metadata]
|
||||
name = ev-dev-tools
|
||||
version = attr: ev_cli.__version__
|
||||
author = aw
|
||||
author_email = aw@pionix.de
|
||||
description = Utilities for developing with the everest framework
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://github.com/EVerest/everest-utils
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
|
||||
[options]
|
||||
install_requires =
|
||||
jinja2 ~=3.1
|
||||
jsonschema ~=4.26
|
||||
stringcase ~=1.2
|
||||
pyyaml ~=6.0
|
||||
package_dir =
|
||||
= src
|
||||
packages = ev_cli
|
||||
python_requires = >=3.7
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
ev-cli = ev_cli.ev:main
|
||||
|
||||
[options.package_data]
|
||||
ev_cli =
|
||||
templates/*.j2
|
||||
licenses/**
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
@@ -0,0 +1,2 @@
|
||||
"""EVerest command line utility."""
|
||||
__version__ = '0.7.3'
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
Provide error parsing functionality.
|
||||
author: andreas.heinrich@pionix.de
|
||||
"""
|
||||
|
||||
from typing import List, NamedTuple
|
||||
from pathlib import Path
|
||||
from . import helpers
|
||||
import yaml, jsonschema
|
||||
|
||||
class ErrorDefinition(NamedTuple):
|
||||
"""Error definition class."""
|
||||
namespace: str
|
||||
name: str
|
||||
description: str
|
||||
|
||||
class ErrorParser:
|
||||
"""Error parser class."""
|
||||
validators = None
|
||||
error_definitions = {}
|
||||
|
||||
@classmethod
|
||||
def load_error_definition_file(cls, path: Path):
|
||||
try:
|
||||
error_def = yaml.safe_load(path.read_text())
|
||||
ErrorParser.validators['error_declaration_list'].validate(error_def)
|
||||
except OSError as err:
|
||||
raise Exception(f'Could not open error definition file {err.filename}: {err.strerror}') from err
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse error definition file {path}: {err}') from err
|
||||
except jsonschema.ValidationError as err:
|
||||
raise Exception(f'Error definition file {path} is not valid: {err}') from err
|
||||
|
||||
namespace = path.stem
|
||||
if namespace in cls.error_definitions:
|
||||
raise Exception(f'Error definition namespace { namespace } already exists.')
|
||||
else:
|
||||
cls.error_definitions[namespace] = {}
|
||||
for entry in error_def['errors']:
|
||||
error = ErrorDefinition(namespace, entry['name'], entry['description'])
|
||||
if error.name in cls.error_definitions[error.namespace]:
|
||||
raise Exception(f'Error definition { error.namespace }/{ error.name } already exists.')
|
||||
cls.error_definitions[error.namespace][error.name] = error
|
||||
|
||||
@classmethod
|
||||
def get_error_definition(cls, namespace: str, name: str) -> ErrorDefinition:
|
||||
"""Get error definition for the provided namespace and name."""
|
||||
if namespace not in cls.error_definitions:
|
||||
path = helpers.resolve_everest_dir_path(f'errors/{ namespace }.yaml')
|
||||
cls.load_error_definition_file(path)
|
||||
if name not in cls.error_definitions[namespace]:
|
||||
raise Exception(f'Error definition { namespace }/{ name } does not exist.')
|
||||
return cls.error_definitions[namespace][name]
|
||||
|
||||
@classmethod
|
||||
def get_error_definitions(cls, namespace: str) -> List[ErrorDefinition]:
|
||||
"""Get error definitions for the provided namespace."""
|
||||
if namespace not in cls.error_definitions:
|
||||
path = helpers.resolve_everest_dir_path(f'errors/{ namespace }.yaml')
|
||||
cls.load_error_definition_file(path)
|
||||
result = []
|
||||
for error in cls.error_definitions[namespace].values():
|
||||
result.append(error)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def resolve_error_reference(cls, error_ref: str) -> List[ErrorDefinition]:
|
||||
"""Resolve error reference."""
|
||||
ref_prefix = '/errors/'
|
||||
if not error_ref.startswith(ref_prefix):
|
||||
raise Exception(f'Error reference { error_ref } does not start with { ref_prefix }.')
|
||||
error_ref = error_ref[len(ref_prefix):]
|
||||
if '#/' in error_ref:
|
||||
namespace, name = error_ref.split('#/')
|
||||
result = []
|
||||
result.append(cls.get_error_definition(namespace, name))
|
||||
return result
|
||||
else:
|
||||
return cls.get_error_definitions(error_ref)
|
||||
902
tools/EVerest-main/applications/utils/ev-dev-tools/src/ev_cli/ev.py
Executable file
902
tools/EVerest-main/applications/utils/ev-dev-tools/src/ev_cli/ev.py
Executable file
@@ -0,0 +1,902 @@
|
||||
#!/usr/bin/env -S python3 -tt
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: aw@pionix.de
|
||||
FIXME (aw): Module documentation.
|
||||
"""
|
||||
|
||||
from ev_cli import __version__
|
||||
from ev_cli import helpers
|
||||
from ev_cli.type_parsing import TypeParser
|
||||
from ev_cli.error_parsing import ErrorParser
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import jinja2 as j2
|
||||
import argparse
|
||||
import stringcase
|
||||
from typing import List
|
||||
|
||||
|
||||
# FIXME (aw): remove these global variables
|
||||
|
||||
# global variables
|
||||
everest_dirs: List[Path] = []
|
||||
work_dir: Path = None
|
||||
|
||||
# jinja template environment and global variable
|
||||
env = j2.Environment(loader=j2.FileSystemLoader(Path(__file__).parent / 'templates'),
|
||||
lstrip_blocks=True, trim_blocks=True, undefined=j2.StrictUndefined,
|
||||
keep_trailing_newline=True)
|
||||
|
||||
templates = {}
|
||||
validators = {}
|
||||
|
||||
# Function declarations
|
||||
|
||||
|
||||
def setup_jinja_env():
|
||||
env.globals['timestamp'] = datetime.utcnow()
|
||||
# FIXME (aw): which repo to use? everest or everest-framework?
|
||||
env.filters['snake_case'] = helpers.snake_case
|
||||
env.filters['create_dummy_result'] = helpers.create_dummy_result
|
||||
|
||||
templates.update({
|
||||
'interface_base': env.get_template('interface-Base.hpp.j2'),
|
||||
'interface_exports': env.get_template('interface-Exports.hpp.j2'),
|
||||
'interface_impl.hpp': env.get_template('interface-Impl.hpp.j2'),
|
||||
'interface_impl.cpp': env.get_template('interface-Impl.cpp.j2'),
|
||||
'types.hpp': env.get_template('types.hpp.j2'),
|
||||
'module.hpp': env.get_template('module.hpp.j2'),
|
||||
'module.cpp': env.get_template('module.cpp.j2'),
|
||||
'ld-ev.hpp': env.get_template('ld-ev.hpp.j2'),
|
||||
'ld-ev.cpp': env.get_template('ld-ev.cpp.j2'),
|
||||
'cmakelists': env.get_template('CMakeLists.txt.j2'),
|
||||
'index.rst': env.get_template('index.rst.j2'),
|
||||
})
|
||||
|
||||
|
||||
def generate_tmpl_data_for_if(interface, if_def, type_file):
|
||||
helpers.parsed_enums.clear()
|
||||
helpers.parsed_types.clear()
|
||||
helpers.type_headers.clear()
|
||||
types = []
|
||||
enums = []
|
||||
vars = []
|
||||
for var, var_info in if_def.get('vars', {}).items():
|
||||
(type_info, enum_info) = helpers.extended_build_type_info(var, var_info, type_file)
|
||||
if enum_info and type_file:
|
||||
enums.append(enum_info)
|
||||
|
||||
vars.append(type_info)
|
||||
|
||||
cmds = []
|
||||
for cmd, cmd_info in if_def.get('cmds', {}).items():
|
||||
args = []
|
||||
for arg, arg_info in cmd_info.get('arguments', {}).items():
|
||||
(type_info, enum_info) = helpers.extended_build_type_info(arg, arg_info, type_file)
|
||||
if enum_info and type_file:
|
||||
enums.append(enum_info)
|
||||
|
||||
args.append(type_info)
|
||||
|
||||
result_type_info = None
|
||||
if 'result' in cmd_info:
|
||||
result_info = cmd_info['result']
|
||||
|
||||
(result_type_info, enum_info) = helpers.extended_build_type_info('result', result_info, type_file)
|
||||
if enum_info and type_file:
|
||||
enums.append(enum_info)
|
||||
|
||||
cmds.append({'name': cmd, 'args': args, 'result': result_type_info})
|
||||
|
||||
if type_file:
|
||||
for parsed_enum in helpers.parsed_enums:
|
||||
enum_info = {
|
||||
'name': parsed_enum['name'],
|
||||
'description': parsed_enum['description'],
|
||||
'enum_type': stringcase.capitalcase(parsed_enum['name']),
|
||||
'enum': parsed_enum['enums']
|
||||
}
|
||||
enums.append(enum_info)
|
||||
|
||||
if type_file:
|
||||
for parsed_type in helpers.parsed_types:
|
||||
parsed_type['name'] = stringcase.capitalcase(parsed_type['name'])
|
||||
if 'properties' in parsed_type:
|
||||
for prop in parsed_type['properties']:
|
||||
if 'type_dict' in prop['info']:
|
||||
path = Path('generated/types') / \
|
||||
prop['info']['type_dict']['type_relative_path'].with_suffix('.hpp')
|
||||
helpers.type_headers.add(path.as_posix())
|
||||
|
||||
types.append(parsed_type)
|
||||
|
||||
error_lists = if_def.get('errors', [])
|
||||
# Use a dict to avoid duplicate error definitions
|
||||
errors_dict = {}
|
||||
for entry in error_lists:
|
||||
if not 'reference' in entry:
|
||||
raise Exception(f'Error definition {entry} does not have a reference.')
|
||||
for error in ErrorParser.resolve_error_reference(entry['reference']):
|
||||
if error.namespace not in errors_dict:
|
||||
errors_dict[error.namespace] = {}
|
||||
if error.name in errors_dict[error.namespace]:
|
||||
raise Exception(f'Error definition {error.namespace}/{error.name} already referenced.')
|
||||
errors_dict[error.namespace][error.name] = error
|
||||
errors = []
|
||||
for value in errors_dict.values():
|
||||
errors.extend(value.values())
|
||||
|
||||
tmpl_data = {
|
||||
'info': {
|
||||
'base_class_header': f'generated/interfaces/{interface}/Implementation.hpp',
|
||||
'interface': interface,
|
||||
'desc': if_def['description'],
|
||||
'type_headers': sorted(helpers.type_headers)
|
||||
},
|
||||
'enums': enums,
|
||||
'types': types,
|
||||
'vars': vars,
|
||||
'cmds': cmds,
|
||||
'errors': errors,
|
||||
}
|
||||
|
||||
return tmpl_data
|
||||
|
||||
|
||||
def generate_tmpl_data_for_module(module, module_def):
|
||||
provides = []
|
||||
for impl, impl_info in module_def.get('provides', {}).items():
|
||||
config = []
|
||||
for conf_id, conf_info in impl_info.get('config', {}).items():
|
||||
type_info = helpers.build_type_info(conf_id, conf_info['type'])
|
||||
config.append(type_info)
|
||||
|
||||
provides.append({
|
||||
'id': impl,
|
||||
'type': impl_info['interface'],
|
||||
'desc': impl_info['description'],
|
||||
'config': config,
|
||||
'class_name': f'{impl_info["interface"]}Impl',
|
||||
'base_class': f'{impl_info["interface"]}ImplBase',
|
||||
'base_class_header': f'generated/interfaces/{impl_info["interface"]}/Implementation.hpp'
|
||||
})
|
||||
|
||||
requires = []
|
||||
for requirement_id, req_info in module_def.get('requires', {}).items():
|
||||
# min_connections=1 and max_connections=1 is the default if not provided otherwise (see manifest meta schema)
|
||||
is_vector = not (
|
||||
('min_connections' not in req_info or req_info['min_connections'] == 1) and
|
||||
('max_connections' not in req_info or req_info['max_connections'] == 1))
|
||||
requires.append({
|
||||
'id': requirement_id,
|
||||
'is_vector': is_vector,
|
||||
'type': req_info['interface'],
|
||||
'class_name': f'{req_info["interface"]}Intf',
|
||||
'exports_header': f'generated/interfaces/{req_info["interface"]}/Interface.hpp'
|
||||
})
|
||||
|
||||
module_config = []
|
||||
for conf_id, conf_info in module_def.get('config', {}).items():
|
||||
type_info = helpers.build_type_info(conf_id, conf_info['type'])
|
||||
module_config.append(type_info)
|
||||
|
||||
tmpl_data = {
|
||||
'info': {
|
||||
'name': module,
|
||||
'class_name': module, # FIXME (aw): enforce capital case?
|
||||
'desc': module_def['description'],
|
||||
'module_header': f'{module}.hpp',
|
||||
'module_config': module_config,
|
||||
'ld_ev_header': 'ld-ev.hpp',
|
||||
'enable_external_mqtt': module_def.get('enable_external_mqtt', False),
|
||||
'enable_telemetry': module_def.get('enable_telemetry', False),
|
||||
'enable_global_errors': module_def.get('enable_global_errors', False)
|
||||
},
|
||||
'provides': provides,
|
||||
'requires': requires,
|
||||
}
|
||||
|
||||
return tmpl_data
|
||||
|
||||
|
||||
def construct_impl_file_paths(impl):
|
||||
interface = impl['type']
|
||||
common_part = f'{impl["id"]}/{interface}'
|
||||
return (f'{common_part}Impl.hpp', f'{common_part}Impl.cpp')
|
||||
|
||||
|
||||
def set_impl_specific_path_vars(tmpl_data, output_path):
|
||||
"""Set cpp_file_rel_path and class_header vars to implementation template data."""
|
||||
for impl in tmpl_data['provides']:
|
||||
(impl['class_header'], impl['cpp_file_rel_path']) = construct_impl_file_paths(impl)
|
||||
|
||||
|
||||
def generate_module_loader_files(rel_mod_dir, output_dir):
|
||||
loader_files = []
|
||||
(_, _, mod) = rel_mod_dir.rpartition('/')
|
||||
|
||||
mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
|
||||
if not mod_path.exists():
|
||||
raise Exception(f'Could not find module manifest ({mod_path}')
|
||||
|
||||
mod_def = helpers.load_validated_module_def(mod_path, validators['module'])
|
||||
tmpl_data = generate_tmpl_data_for_module(mod, mod_def)
|
||||
|
||||
set_impl_specific_path_vars(tmpl_data, mod_path.parent)
|
||||
|
||||
# ld-ev.hpp
|
||||
tmpl_data['info']['hpp_guard'] = 'LD_EV_HPP'
|
||||
|
||||
loader_files.append({
|
||||
'filename': 'ld-ev.hpp',
|
||||
'path': output_dir / mod / 'ld-ev.hpp',
|
||||
'printable_name': f'{mod}/ld-ev.hpp',
|
||||
'content': templates['ld-ev.hpp'].render(tmpl_data),
|
||||
'template_path': Path(templates['ld-ev.hpp'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime
|
||||
})
|
||||
|
||||
# ld-ev.cpp
|
||||
loader_files.append({
|
||||
'filename': 'ld-ev.cpp',
|
||||
'path': output_dir / mod / 'ld-ev.cpp',
|
||||
'printable_name': f'{mod}/ld-ev.cpp',
|
||||
'content': templates['ld-ev.cpp'].render(tmpl_data),
|
||||
'template_path': Path(templates['ld-ev.cpp'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime
|
||||
})
|
||||
|
||||
return loader_files
|
||||
|
||||
|
||||
def generate_module_files(rel_mod_dir, update_flag, licenses):
|
||||
(_, _, mod) = rel_mod_dir.rpartition('/')
|
||||
|
||||
mod_files = {'core': [], 'interfaces': [], 'docs': []}
|
||||
mod_path = work_dir / f'modules/{rel_mod_dir}/manifest.yaml'
|
||||
mod_def = helpers.load_validated_module_def(mod_path, validators['module'])
|
||||
|
||||
default_license_dir = Path(__file__).parent / 'licenses'
|
||||
current_license_dir = work_dir / 'licenses'
|
||||
additional_license_dir = Path(licenses)
|
||||
license_dirs = [default_license_dir, current_license_dir, additional_license_dir]
|
||||
license_url = mod_def['metadata']['license']
|
||||
license_header = helpers.get_license_header(license_dirs, license_url)
|
||||
|
||||
if not license_header:
|
||||
print(f'Could not find license "{license_url}" in {license_dirs}.')
|
||||
print('Consider providing a additonal custom license directory with --licenses')
|
||||
exit(1)
|
||||
|
||||
tmpl_data = generate_tmpl_data_for_module(mod, mod_def)
|
||||
output_path = mod_path.parent
|
||||
# FIXME (aw): we might move the following function into generate_tmp_data_for_module
|
||||
set_impl_specific_path_vars(tmpl_data, output_path)
|
||||
|
||||
cmakelists_blocks = {
|
||||
'version': 'v1',
|
||||
'format_str': '# ev@{uuid}:{version}',
|
||||
'regex_str': '^(?P<indent>\s*)# ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
|
||||
'definitions': {
|
||||
'add_general': {
|
||||
'id': 'bcc62523-e22b-41d7-ba2f-825b493a3c97',
|
||||
'content': '# insert your custom targets and additional config variables here'
|
||||
},
|
||||
'add_other': {
|
||||
'id': 'c55432ab-152c-45a9-9d2e-7281d50c69c3',
|
||||
'content': '# insert other things like install cmds etc here'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_hpp_blocks = {
|
||||
'version': 'v1',
|
||||
'format_str': '// ev@{uuid}:{version}',
|
||||
'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
|
||||
'definitions': {
|
||||
'add_headers': {
|
||||
'id': '75ac1216-19eb-4182-a85c-820f1fc2c091',
|
||||
'content': '// insert your custom include headers here'
|
||||
},
|
||||
'public_defs': {
|
||||
'id': '8ea32d28-373f-4c90-ae5e-b4fcc74e2a61',
|
||||
'content': '// insert your public definitions here'
|
||||
},
|
||||
'protected_defs': {
|
||||
'id': 'd2d1847a-7b88-41dd-ad07-92785f06f5c4',
|
||||
'content': '// insert your protected definitions here'
|
||||
},
|
||||
'private_defs': {
|
||||
'id': '3370e4dd-95f4-47a9-aaec-ea76f34a66c9',
|
||||
'content': '// insert your private definitions here'
|
||||
},
|
||||
'after_class': {
|
||||
'id': '3d7da0ad-02c2-493d-9920-0bbbd56b9876',
|
||||
'content': '// insert other definitions here'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod_hpp_blocks = {
|
||||
'version': 'v1',
|
||||
'format_str': '// ev@{uuid}:{version}',
|
||||
'regex_str': '^(?P<indent>\s*)// ev@(?P<uuid>[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}):(?P<version>.*)$',
|
||||
'definitions': {
|
||||
'add_headers': {
|
||||
'id': '4bf81b14-a215-475c-a1d3-0a484ae48918',
|
||||
'content': '// insert your custom include headers here'
|
||||
},
|
||||
'public_defs': {
|
||||
'id': '1fce4c5e-0ab8-41bb-90f7-14277703d2ac',
|
||||
'content': '// insert your public definitions here'
|
||||
},
|
||||
'protected_defs': {
|
||||
'id': '4714b2ab-a24f-4b95-ab81-36439e1478de',
|
||||
'content': '// insert your protected definitions here'
|
||||
},
|
||||
'private_defs': {
|
||||
'id': '211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8',
|
||||
'content': '// insert your private definitions here'
|
||||
},
|
||||
'after_class': {
|
||||
'id': '087e516b-124c-48df-94fb-109508c7cda9',
|
||||
'content': '// insert other definitions here'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# provided interface implementations (impl cpp & hpp)
|
||||
for impl in tmpl_data['provides']:
|
||||
interface = impl['type']
|
||||
(impl_hpp_file, impl_cpp_file) = construct_impl_file_paths(impl)
|
||||
|
||||
# load template data for interface
|
||||
if_def, last_mtime = load_interface_definition(interface)
|
||||
|
||||
if_tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)
|
||||
|
||||
if_tmpl_data['info'].update({
|
||||
'hpp_guard': helpers.snake_case(f'{impl["id"]}_{interface}').upper() + '_IMPL_HPP',
|
||||
'config': impl['config'],
|
||||
'class_name': interface + 'Impl',
|
||||
'class_parent': interface + 'ImplBase',
|
||||
'module_header': f'../{mod}.hpp',
|
||||
'module_class': mod,
|
||||
'interface_implementation_id': impl['id']
|
||||
})
|
||||
|
||||
if_tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(
|
||||
impl_hpp_blocks, output_path / impl_hpp_file, update_flag)
|
||||
if_tmpl_data['info']['license_header'] = license_header
|
||||
|
||||
# FIXME (aw): time stamp should include parent interfaces modification dates
|
||||
mod_files['interfaces'].append({
|
||||
'abbr': f'{impl["id"]}.hpp',
|
||||
'path': output_path / impl_hpp_file,
|
||||
'printable_name': impl_hpp_file,
|
||||
'content': templates['interface_impl.hpp'].render(if_tmpl_data),
|
||||
'template_path': Path(templates['interface_impl.hpp'].filename),
|
||||
'last_mtime': last_mtime,
|
||||
'license_header': license_header
|
||||
})
|
||||
|
||||
mod_files['interfaces'].append({
|
||||
'abbr': f'{impl["id"]}.cpp',
|
||||
'path': output_path / impl_cpp_file,
|
||||
'printable_name': impl_cpp_file,
|
||||
'content': templates['interface_impl.cpp'].render(if_tmpl_data),
|
||||
'template_path': Path(templates['interface_impl.cpp'].filename),
|
||||
'last_mtime': last_mtime,
|
||||
'license_header': license_header
|
||||
})
|
||||
|
||||
cmakelists_file = output_path / 'CMakeLists.txt'
|
||||
tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(cmakelists_blocks, cmakelists_file, update_flag)
|
||||
tmpl_data['info']['license_header'] = license_header
|
||||
mod_files['core'].append({
|
||||
'abbr': 'cmakelists',
|
||||
'path': cmakelists_file,
|
||||
'content': templates['cmakelists'].render(tmpl_data),
|
||||
'template_path': Path(templates['cmakelists'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime
|
||||
})
|
||||
|
||||
# module.hpp
|
||||
tmpl_data['info']['hpp_guard'] = helpers.snake_case(mod).upper() + '_HPP'
|
||||
mod_hpp_file = output_path / f'{mod}.hpp'
|
||||
tmpl_data['info']['blocks'] = helpers.load_tmpl_blocks(mod_hpp_blocks, mod_hpp_file, update_flag)
|
||||
mod_files['core'].append({
|
||||
'abbr': 'module.hpp',
|
||||
'path': mod_hpp_file,
|
||||
'content': templates['module.hpp'].render(tmpl_data),
|
||||
'template_path': Path(templates['module.hpp'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime,
|
||||
'license_header': license_header
|
||||
})
|
||||
|
||||
# module.cpp
|
||||
mod_cpp_file = output_path / f'{mod}.cpp'
|
||||
mod_files['core'].append({
|
||||
'abbr': 'module.cpp',
|
||||
'path': mod_cpp_file,
|
||||
'content': templates['module.cpp'].render(tmpl_data),
|
||||
'template_path': Path(templates['module.cpp'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime,
|
||||
'license_header': license_header
|
||||
})
|
||||
|
||||
# docs/index.rst
|
||||
mod_files['docs'].append({
|
||||
'abbr': 'index.rst',
|
||||
'path': output_path / 'docs' / 'index.rst',
|
||||
'content': templates['index.rst'].render(tmpl_data),
|
||||
'template_path': Path(templates['index.rst'].filename),
|
||||
'last_mtime': mod_path.stat().st_mtime
|
||||
})
|
||||
|
||||
for file_info in [*mod_files['core'], *mod_files['interfaces'], *mod_files['docs']]:
|
||||
file_info['printable_name'] = file_info['path'].relative_to(output_path)
|
||||
|
||||
return mod_files
|
||||
|
||||
|
||||
def load_interface_definition(interface):
|
||||
if_path = helpers.resolve_everest_dir_path(f'interfaces/{interface}.yaml')
|
||||
|
||||
if_def = helpers.load_validated_interface_def(if_path, validators['interface'])
|
||||
|
||||
if 'vars' not in if_def:
|
||||
if_def['vars'] = {}
|
||||
if 'cmds' not in if_def:
|
||||
if_def['cmds'] = {}
|
||||
|
||||
last_mtime = if_path.stat().st_mtime
|
||||
|
||||
return if_def, last_mtime
|
||||
|
||||
|
||||
def generate_interface_headers(interface, all_interfaces_flag, output_dir):
|
||||
if_parts = {'base': None, 'exports': None, 'types': None}
|
||||
|
||||
try:
|
||||
if_def, last_mtime = load_interface_definition(interface)
|
||||
except Exception as e:
|
||||
if not all_interfaces_flag:
|
||||
raise
|
||||
else:
|
||||
# FIXME (aw): should we really silently ignore that?
|
||||
print(f'Ignoring interface {interface} with reason: {e}')
|
||||
return
|
||||
|
||||
tmpl_data = generate_tmpl_data_for_if(interface, if_def, False)
|
||||
|
||||
output_path = output_dir / interface
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
tmpl_data['info']['interface_name'] = f'{interface}'
|
||||
tmpl_data['info']['namespace'] = [f'{interface}']
|
||||
|
||||
# generate Base file (providers view)
|
||||
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_IMPLEMENTATION_HPP'
|
||||
tmpl_data['info']['class_name'] = f'{interface}ImplBase'
|
||||
|
||||
base_file = output_path / 'Implementation.hpp'
|
||||
|
||||
if_parts['base'] = {
|
||||
'path': base_file,
|
||||
'content': templates['interface_base'].render(tmpl_data),
|
||||
'template_path': Path(templates['interface_base'].filename),
|
||||
'last_mtime': last_mtime,
|
||||
'printable_name': base_file.relative_to(output_path.parent)
|
||||
}
|
||||
|
||||
# generate Exports file (users view)
|
||||
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_INTERFACE_HPP'
|
||||
tmpl_data['info']['class_name'] = f'{interface}Intf'
|
||||
|
||||
exports_file = output_path / 'Interface.hpp'
|
||||
|
||||
if_parts['exports'] = {
|
||||
'path': exports_file,
|
||||
'content': templates['interface_exports'].render(tmpl_data),
|
||||
'template_path': Path(templates['interface_exports'].filename),
|
||||
'last_mtime': last_mtime,
|
||||
'printable_name': exports_file.relative_to(output_path.parent)
|
||||
}
|
||||
|
||||
# generate Types file
|
||||
tmpl_data['info']['hpp_guard'] = helpers.snake_case(interface).upper() + '_TYPES_HPP'
|
||||
|
||||
types_file = output_path / 'Types.hpp'
|
||||
|
||||
if_parts['types'] = {
|
||||
'path': types_file,
|
||||
'content': templates['types.hpp'].render(tmpl_data),
|
||||
'template_path': Path(templates['types.hpp'].filename),
|
||||
'last_mtime': last_mtime,
|
||||
'printable_name': types_file.relative_to(output_path.parent)
|
||||
}
|
||||
|
||||
return if_parts
|
||||
|
||||
|
||||
def module_create(args):
|
||||
create_strategy = 'force-create' if args.force else 'create'
|
||||
|
||||
detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
|
||||
if detected_projects:
|
||||
helpers.everest_dirs.extend(detected_projects)
|
||||
|
||||
mod_files = generate_module_files(args.module, False, args.licenses)
|
||||
|
||||
if args.only == 'which':
|
||||
helpers.print_available_mod_files(mod_files)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
helpers.filter_mod_files(args.only, mod_files)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return
|
||||
|
||||
for file_info in mod_files['core'] + mod_files['interfaces'] + mod_files['docs']:
|
||||
if not args.disable_clang_format:
|
||||
helpers.clang_format(args.clang_format_file, file_info)
|
||||
|
||||
helpers.write_content_to_file(file_info, create_strategy, args.diff)
|
||||
|
||||
|
||||
def module_update(args):
|
||||
detected_projects = helpers.detect_everest_projects(args.everest_projects, args.build_dir)
|
||||
if detected_projects:
|
||||
helpers.everest_dirs.extend(detected_projects)
|
||||
|
||||
# Always generate type info before updating module
|
||||
for type_with_namespace in list_types_with_namespace():
|
||||
_tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)
|
||||
|
||||
primary_update_strategy = 'force-update' if args.force else 'update'
|
||||
update_strategy = {'module.cpp': 'update-if-non-existent'}
|
||||
for file_name in ['cmakelists', 'module.hpp']:
|
||||
update_strategy[file_name] = primary_update_strategy
|
||||
|
||||
# FIXME (aw): refactor out this only handling and rename it properly
|
||||
mod_files = generate_module_files(args.module, True, args.licenses)
|
||||
|
||||
if args.only == 'which':
|
||||
helpers.print_available_mod_files(mod_files)
|
||||
return
|
||||
else:
|
||||
try:
|
||||
helpers.filter_mod_files(args.only, mod_files)
|
||||
except Exception as err:
|
||||
print(err)
|
||||
return
|
||||
|
||||
if not args.disable_clang_format:
|
||||
for file_info in mod_files['core'] + mod_files['interfaces']:
|
||||
helpers.clang_format(args.clang_format_file, file_info)
|
||||
|
||||
for file_info in mod_files['core']:
|
||||
helpers.write_content_to_file(file_info, update_strategy[file_info['abbr']], args.diff, '', True)
|
||||
|
||||
for file_info in mod_files['interfaces']:
|
||||
if file_info['abbr'].endswith('.hpp'):
|
||||
helpers.write_content_to_file(file_info, primary_update_strategy, args.diff, '', True)
|
||||
else:
|
||||
helpers.write_content_to_file(file_info, 'update-if-non-existent', args.diff, '', True)
|
||||
|
||||
|
||||
def module_genld(args):
|
||||
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
|
||||
'build/generated/generated/modules'
|
||||
primary_update_strategy = 'force-update' if args.force else 'update'
|
||||
|
||||
loader_files = generate_module_loader_files(args.module, output_dir)
|
||||
|
||||
if not args.disable_clang_format:
|
||||
for file_info in loader_files:
|
||||
helpers.clang_format(args.clang_format_file, file_info)
|
||||
|
||||
for file_info in loader_files:
|
||||
helpers.write_content_to_file_and_check_template(file_info, primary_update_strategy)
|
||||
|
||||
|
||||
def module_get_templates(args):
|
||||
interface_files = args.separator.join(
|
||||
[templates['ld-ev.hpp'].filename,
|
||||
templates['ld-ev.cpp'].filename])
|
||||
|
||||
print(f'{interface_files}')
|
||||
|
||||
|
||||
def interface_genhdr(args):
|
||||
# Always generate type info before generating interfaces
|
||||
for type_with_namespace in list_types_with_namespace():
|
||||
_tmpl_data, _last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types=True)
|
||||
|
||||
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
|
||||
'build/generated/include/generated/interfaces'
|
||||
primary_update_strategy = 'force-update' if args.force else 'update'
|
||||
|
||||
interfaces = args.interfaces
|
||||
all_interfaces = False
|
||||
if not interfaces:
|
||||
all_interfaces = True
|
||||
interfaces = []
|
||||
for everest_dir in everest_dirs:
|
||||
if_dir = everest_dir / 'interfaces'
|
||||
interfaces += [if_path.stem for if_path in if_dir.iterdir() if (if_path.is_file()
|
||||
and if_path.suffix == '.yaml')]
|
||||
|
||||
for interface in interfaces:
|
||||
if_parts = generate_interface_headers(interface, all_interfaces, output_dir)
|
||||
|
||||
if not args.disable_clang_format:
|
||||
# FIXME (aw): this broken, because in case all_interfaces is true, if_parts might be none for invalid interface files
|
||||
helpers.clang_format(args.clang_format_file, if_parts['base'])
|
||||
helpers.clang_format(args.clang_format_file, if_parts['exports'])
|
||||
helpers.clang_format(args.clang_format_file, if_parts['types'])
|
||||
|
||||
helpers.write_content_to_file_and_check_template(if_parts['base'], primary_update_strategy, args.diff)
|
||||
helpers.write_content_to_file_and_check_template(if_parts['exports'], primary_update_strategy, args.diff)
|
||||
helpers.write_content_to_file_and_check_template(if_parts['types'], primary_update_strategy, args.diff)
|
||||
|
||||
|
||||
def interface_get_templates(args):
|
||||
interface_files = args.separator.join(
|
||||
[templates['interface_base'].filename,
|
||||
templates['interface_exports'].filename])
|
||||
|
||||
print(f'{interface_files}')
|
||||
|
||||
|
||||
def helpers_genuuids(args):
|
||||
if (args.count <= 0):
|
||||
raise Exception(f'Invalid number ("{args.count}") of uuids to generate')
|
||||
helpers.generate_some_uuids(args.count)
|
||||
|
||||
|
||||
def helpers_yaml2json(args):
|
||||
helpers.yaml2json(Path(args.input).resolve(), Path(args.output).resolve())
|
||||
|
||||
|
||||
def helpers_json2yaml(args):
|
||||
helpers.json2yaml(Path(args.input).resolve(), Path(args.output).resolve())
|
||||
|
||||
|
||||
def list_types_with_namespace(types=None) -> List:
|
||||
if not types:
|
||||
types = []
|
||||
for everest_dir in everest_dirs:
|
||||
types_dir = everest_dir / 'types'
|
||||
types += list(types_dir.glob('**/*.yaml'))
|
||||
|
||||
types_with_namespace = []
|
||||
for type_path in types:
|
||||
relative_path = None
|
||||
for everest_dir in everest_dirs:
|
||||
types_dir = everest_dir / 'types'
|
||||
try:
|
||||
relative_path = type_path.relative_to(types_dir).with_suffix('')
|
||||
break
|
||||
except ValueError:
|
||||
pass
|
||||
uppercase_path = []
|
||||
for part in relative_path.parts:
|
||||
uppercase_path.append(stringcase.capitalcase(part))
|
||||
namespace = '::'.join(relative_path.parts)
|
||||
type_with_namespace = {
|
||||
'path': type_path,
|
||||
'relative_path': relative_path,
|
||||
'namespace': namespace,
|
||||
'uppercase_path': uppercase_path,
|
||||
}
|
||||
|
||||
types_with_namespace.append(type_with_namespace)
|
||||
|
||||
return types_with_namespace
|
||||
|
||||
|
||||
def types_genhdr(args):
|
||||
print('Generating global type headers.')
|
||||
output_dir = Path(args.output_dir).resolve() if args.output_dir else work_dir / \
|
||||
'build/generated/generated/types'
|
||||
|
||||
primary_update_strategy = 'force-update' if args.force else 'update'
|
||||
|
||||
types = None
|
||||
all_types = False
|
||||
if 'types' not in args:
|
||||
all_types = True
|
||||
else:
|
||||
types = args.types
|
||||
|
||||
types_with_namespace = list_types_with_namespace(types=types)
|
||||
|
||||
for type_with_namespace in types_with_namespace:
|
||||
type_parts = TypeParser.generate_type_headers(type_with_namespace, all_types, output_dir)
|
||||
|
||||
if not args.disable_clang_format:
|
||||
helpers.clang_format(args.clang_format_file, type_parts['types'])
|
||||
|
||||
helpers.write_content_to_file_and_check_template(type_parts['types'], primary_update_strategy, args.diff)
|
||||
|
||||
|
||||
def types_get_templates(args):
|
||||
interface_files = templates['types.hpp'].filename
|
||||
|
||||
print(f'{interface_files}')
|
||||
|
||||
|
||||
def main():
|
||||
global validators, everest_dirs, work_dir
|
||||
everest_dir = Path(__file__).parent.parent.parent.parent.parent.parent
|
||||
everest_framework_dir = everest_dir / 'lib' / 'everest' / 'framework'
|
||||
schemas_dir = everest_framework_dir / 'schemas'
|
||||
everest_dir_default = [str(everest_dir), str(Path.cwd().parent / 'EVerest')]
|
||||
|
||||
parser = argparse.ArgumentParser(description='Everest command line tool')
|
||||
parser.add_argument('--version', action='version', version=f'%(prog)s {__version__}')
|
||||
|
||||
common_parser = argparse.ArgumentParser(add_help=False)
|
||||
|
||||
common_parser.add_argument('--work-dir', '-wd', type=str,
|
||||
help='work directory containing the manifest definitions (default: .)', default=str(Path.cwd()))
|
||||
common_parser.add_argument('--everest-dir', '-ed', nargs='*',
|
||||
help=f'everest directory containing the interface definitions (default: {everest_dir_default})',
|
||||
default=everest_dir_default)
|
||||
common_parser.add_argument('--everest-projects', '-ep', nargs='*',
|
||||
help='everest project names. used in auto detection of their directories to get eg. interface defintions (default: EVerest)',
|
||||
default=['EVerest'])
|
||||
common_parser.add_argument('--schemas-dir', '-sd', type=str,
|
||||
help=f'everest framework directory containing the schema definitions (default: {schemas_dir})',
|
||||
default=str(schemas_dir))
|
||||
common_parser.add_argument('--licenses', '-lc', type=str,
|
||||
help='license directory from which ev-cli will attempt to parse custom license texts (default ../licenses)',
|
||||
default=str(Path.cwd() / '../licenses'))
|
||||
common_parser.add_argument('--build-dir', '-bd', type=str,
|
||||
help='everest build directory from which ev-cli will attempt to parse the everest framework schema definitions (default ./build)',
|
||||
default=str(Path.cwd() / 'build'))
|
||||
common_parser.add_argument('--clang-format-file', type=str, default=str(Path.cwd()),
|
||||
help='Path to the directory, containing the .clang-format file (default: .)')
|
||||
common_parser.add_argument('--disable-clang-format', action='store_true', default=False,
|
||||
help='Set this flag to disable clang-format')
|
||||
|
||||
subparsers = parser.add_subparsers(metavar='<command>', help='available commands', required=True)
|
||||
parser_mod = subparsers.add_parser('module', aliases=['mod'], help='module related actions')
|
||||
parser_if = subparsers.add_parser('interface', aliases=['if'], help='interface related actions')
|
||||
parser_hlp = subparsers.add_parser('helpers', aliases=['hlp'], help='helper actions')
|
||||
parser_types = subparsers.add_parser('types', aliases=['ty'], help='type related actions')
|
||||
|
||||
mod_actions = parser_mod.add_subparsers(metavar='<action>', help='available actions', required=True)
|
||||
mod_create_parser = mod_actions.add_parser('create', aliases=['c'], parents=[
|
||||
common_parser], help='create module(s)')
|
||||
mod_create_parser.add_argument('module', type=str, help='name of the module, that should be created')
|
||||
mod_create_parser.add_argument('-f', '--force', action='store_true', help='force overwriting - use with care!')
|
||||
mod_create_parser.add_argument('-d', '--diff', '--dry-run', action='store_true',
|
||||
help='show resulting diff on create or overwrite')
|
||||
mod_create_parser.add_argument('--only', type=str,
|
||||
help='Comma separated filter list of module files, that should be created. '
|
||||
'For a list of available files use "--only which".')
|
||||
mod_create_parser.set_defaults(action_handler=module_create)
|
||||
|
||||
mod_update_parser = mod_actions.add_parser('update', aliases=['u'], parents=[
|
||||
common_parser], help='update module(s)')
|
||||
mod_update_parser.add_argument('module', type=str, help='name of the module, that should be updated')
|
||||
mod_update_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
|
||||
mod_update_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
|
||||
mod_update_parser.add_argument('--only', type=str,
|
||||
help='Comma separated filter list of module files, that should be updated. '
|
||||
'For a list of available files use "--only which".')
|
||||
mod_update_parser.set_defaults(action_handler=module_update)
|
||||
|
||||
mod_genld_parser = mod_actions.add_parser(
|
||||
'generate-loader', aliases=['gl'], parents=[common_parser], help='generate everest loader')
|
||||
mod_genld_parser.add_argument(
|
||||
'module', type=str, help='name of the module, for which the loader should be generated')
|
||||
mod_genld_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
|
||||
mod_genld_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated loader '
|
||||
'files (default: {everest-dir}/build/generated/generated/modules)')
|
||||
mod_genld_parser.set_defaults(action_handler=module_genld)
|
||||
|
||||
if_actions = parser_if.add_subparsers(metavar='<action>', help='available actions', required=True)
|
||||
if_genhdr_parser = if_actions.add_parser(
|
||||
'generate-headers', aliases=['gh'], parents=[common_parser], help='generate headers')
|
||||
if_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
|
||||
if_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated interface '
|
||||
'headers (default: {everest-dir}/build/generated/generated/interfaces)')
|
||||
if_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
|
||||
if_genhdr_parser.add_argument('interfaces', nargs='*', help='a list of interfaces, for which header files should '
|
||||
'be generated - if no interface is given, all will be processed and non-processable '
|
||||
'will be skipped')
|
||||
if_genhdr_parser.set_defaults(action_handler=interface_genhdr)
|
||||
|
||||
hlp_actions = parser_hlp.add_subparsers(metavar='<action>', help='available actions', required=True)
|
||||
hlp_genuuid_parser = hlp_actions.add_parser('generate-uuids', help='generate uuids')
|
||||
hlp_genuuid_parser.add_argument('count', type=int, default=3)
|
||||
hlp_genuuid_parser.set_defaults(action_handler=helpers_genuuids)
|
||||
|
||||
hlp_yaml2json_parser = hlp_actions.add_parser('yaml2json', help='convert yaml into json')
|
||||
hlp_yaml2json_parser.add_argument('input', type=str, help='path to yaml input file')
|
||||
hlp_yaml2json_parser.add_argument('output', type=str, help='path to json output file')
|
||||
hlp_yaml2json_parser.set_defaults(action_handler=helpers_yaml2json)
|
||||
|
||||
hlp_json2yaml_parser = hlp_actions.add_parser('json2yaml', help='convert json into yaml')
|
||||
hlp_json2yaml_parser.add_argument('input', type=str, help='path to json input file')
|
||||
hlp_json2yaml_parser.add_argument('output', type=str, help='path to yaml output file')
|
||||
hlp_json2yaml_parser.set_defaults(action_handler=helpers_json2yaml)
|
||||
|
||||
types_actions = parser_types.add_subparsers(metavar='<action>', help='available actions', required=True)
|
||||
types_genhdr_parser = types_actions.add_parser(
|
||||
'generate-headers', aliases=['gh'], parents=[common_parser], help='generate type headers')
|
||||
types_genhdr_parser.add_argument('-f', '--force', action='store_true', help='force overwriting')
|
||||
types_genhdr_parser.add_argument('-o', '--output-dir', type=str, help='Output directory for generated type '
|
||||
'headers (default: {everest-dir}/build/generated/generated/types)')
|
||||
types_genhdr_parser.add_argument('-d', '--diff', '--dry-run', action='store_true', help='show resulting diff')
|
||||
types_genhdr_parser.add_argument('types', nargs='*', help='a list of types, for which header files should '
|
||||
'be generated - if no type is given, all will be processed and non-processable '
|
||||
'will be skipped')
|
||||
types_genhdr_parser.set_defaults(action_handler=types_genhdr)
|
||||
|
||||
for sub_parser, get_template_function in [
|
||||
(mod_actions, module_get_templates),
|
||||
(if_actions, interface_get_templates),
|
||||
(types_actions, types_get_templates)
|
||||
]:
|
||||
get_templates_parser = sub_parser.add_parser(
|
||||
'get-templates', aliases=['gt'], parents=[common_parser], help='get paths to template files')
|
||||
get_templates_parser.add_argument(
|
||||
'-s', '--separator', type=str, default='\n', help='separator between template files')
|
||||
get_templates_parser.set_defaults(action_handler=get_template_function)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if 'everest_dir' in args:
|
||||
# FIXME (aw): the helper commands do not set everest_dir, work_dir and schema_dirs, but the following common
|
||||
# code has to run for all other commands - we need some better check here than just checking for
|
||||
# 'everest_dir' in args!
|
||||
for entry in args.everest_dir:
|
||||
everest_dir = Path(entry).resolve()
|
||||
everest_dirs.append(everest_dir)
|
||||
|
||||
helpers.everest_dirs = everest_dirs
|
||||
|
||||
work_dir = Path(args.work_dir).resolve()
|
||||
|
||||
setup_jinja_env()
|
||||
|
||||
schemas_dir = Path(args.schemas_dir).resolve()
|
||||
if not schemas_dir.exists():
|
||||
print(f'The default ("{schemas_dir}") xor supplied (via --schemas-dir) schemas directory'
|
||||
' doesn\'t exist.\n'
|
||||
f'dir: {schemas_dir}')
|
||||
cmake_cache_path = Path(args.build_dir) / 'CMakeCache.txt'
|
||||
found_dir = helpers.get_path_from_cmake_cache('everest-framework', cmake_cache_path, '--schemas-dir')
|
||||
if not found_dir:
|
||||
exit(1)
|
||||
schemas_dir = found_dir / 'schemas'
|
||||
if not schemas_dir.exists():
|
||||
exit(1)
|
||||
|
||||
validators = helpers.load_validators(schemas_dir)
|
||||
|
||||
TypeParser.validators = validators
|
||||
TypeParser.templates = templates
|
||||
|
||||
ErrorParser.validators = validators
|
||||
|
||||
args.action_handler(args)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main()
|
||||
except helpers.EVerestParsingException as e:
|
||||
raise SystemExit(e)
|
||||
@@ -0,0 +1,817 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: aw@pionix.de
|
||||
FIXME (aw): Module documentation.
|
||||
"""
|
||||
|
||||
from .type_parsing import TypeParser
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import re
|
||||
from typing import Dict, List, Tuple
|
||||
import keyword
|
||||
|
||||
import json
|
||||
import jsonschema
|
||||
|
||||
import yaml
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import stringcase
|
||||
|
||||
|
||||
everest_dirs: List[Path] = []
|
||||
|
||||
|
||||
class EVerestParsingException(SystemExit):
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
out = ''
|
||||
if len(word) == 0:
|
||||
return out
|
||||
cur_char: str = ''
|
||||
for i in range(len(word)):
|
||||
if i == 0:
|
||||
cur_char = word[i]
|
||||
if not cur_char.isalnum():
|
||||
raise Exception('Non legal character in: ' + word)
|
||||
out += cur_char.lower()
|
||||
continue
|
||||
last_char: str = cur_char
|
||||
cur_char = word[i]
|
||||
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
|
||||
|
||||
|
||||
def create_dummy_result(json_type) -> str:
|
||||
def primitive_to_sample_value(type):
|
||||
if type == 'boolean':
|
||||
return 'true'
|
||||
elif type == 'integer':
|
||||
return '42'
|
||||
elif type == 'number':
|
||||
return '3.14'
|
||||
elif type == 'string':
|
||||
return '"everest"'
|
||||
elif type == 'object':
|
||||
return '{}'
|
||||
elif type == 'array':
|
||||
return '{}'
|
||||
else:
|
||||
raise Exception(f'This json type "{type}" is not known or not implemented')
|
||||
|
||||
if isinstance(json_type, list):
|
||||
return '{}' # default initialization for variant
|
||||
else:
|
||||
return primitive_to_sample_value(json_type)
|
||||
|
||||
|
||||
cpp_type_map = {
|
||||
'null': 'std::nullptr_t', # FIXME (aw): who gets the null, json? or the variant
|
||||
'integer': 'int',
|
||||
'number': 'double',
|
||||
'string': 'std::string',
|
||||
'boolean': 'bool',
|
||||
'array': 'Array',
|
||||
'object': 'Object',
|
||||
}
|
||||
|
||||
def clang_format(config_file_path, file_info):
|
||||
# check if we handle cpp and hpp files
|
||||
if not file_info['path'].suffix in ('.hpp', '.cpp'):
|
||||
return
|
||||
|
||||
clang_format_path = shutil.which('clang-format')
|
||||
if clang_format_path is None:
|
||||
raise RuntimeError('Could not find clang-format executable - needed when passing clang-format config file')
|
||||
|
||||
config_file_path = Path(config_file_path)
|
||||
if not config_file_path.is_dir():
|
||||
raise RuntimeError(f'Supplied directory for the clang-format file ({config_file_path}) does not exist')
|
||||
|
||||
if not (config_file_path / '.clang-format').exists():
|
||||
raise RuntimeError(f'Supplied directory for the clang-format file '
|
||||
f'({config_file_path}) does not contain a .clang-format file')
|
||||
|
||||
content = file_info['content']
|
||||
|
||||
run_parms = {'capture_output': True, 'cwd': config_file_path, 'encoding': 'utf-8', 'input': content}
|
||||
|
||||
format_cmd = subprocess.run([clang_format_path, '--style=file'], **run_parms)
|
||||
|
||||
if format_cmd.returncode != 0:
|
||||
raise RuntimeError(f'clang-format failed with:\n{format_cmd.stderr}')
|
||||
|
||||
file_info['content'] = format_cmd.stdout
|
||||
|
||||
|
||||
def resolve_everest_dir_path(postfix):
|
||||
resolved_path = None
|
||||
for everest_dir in everest_dirs:
|
||||
path = everest_dir / postfix
|
||||
if path.exists():
|
||||
resolved_path = path
|
||||
break
|
||||
|
||||
if not resolved_path:
|
||||
raise EVerestParsingException(
|
||||
f'Could not resolve "{postfix}" in any of the provided everest-dir ({everest_dirs}).')
|
||||
|
||||
return resolved_path
|
||||
|
||||
|
||||
def build_type_info(name, json_type):
|
||||
ti = {
|
||||
'name': name,
|
||||
'is_variant': False,
|
||||
'cpp_type': None,
|
||||
'json_type': json_type
|
||||
}
|
||||
|
||||
if isinstance(json_type, list):
|
||||
ti['is_variant'] = True
|
||||
ti['cpp_type'] = [cpp_type_map[e] for e in json_type if e != 'null']
|
||||
ti['cpp_type'].sort() # sort, so template generation might get reduced
|
||||
# prepend boost::blank if type 'null' exists, so the variant
|
||||
# gets default initialized with blank
|
||||
if 'null' in json_type:
|
||||
ti['cpp_type'].insert(0, cpp_type_map['null'])
|
||||
else:
|
||||
ti['cpp_type'] = cpp_type_map[json_type]
|
||||
|
||||
return ti
|
||||
|
||||
|
||||
type_headers = set()
|
||||
parsed_types: List = []
|
||||
parsed_enums: List = []
|
||||
current_defs: Dict = {}
|
||||
|
||||
format_types = dict()
|
||||
# format_types['date-time'] = 'DateTime'
|
||||
|
||||
|
||||
def object_exists(name: str) -> bool:
|
||||
"""Check if an object already exists."""
|
||||
for el in parsed_types:
|
||||
if el['name'] == name:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def add_enum_type(name: str, enums: Tuple[str], description: str):
|
||||
"""Add enum type to parsed_types."""
|
||||
for el in parsed_enums:
|
||||
if el['name'] == name:
|
||||
raise Exception('Warning: enum ' + name + ' already exists')
|
||||
parsed_enums.append({
|
||||
'name': name,
|
||||
'enums': enums,
|
||||
'description': description
|
||||
})
|
||||
|
||||
|
||||
def parse_ref(ref: str, prop_type, prop_info: Dict) -> Tuple[str, dict]:
|
||||
if ref not in TypeParser.all_types:
|
||||
TypeParser.all_types[ref] = TypeParser.parse_type_url(type_url=ref)
|
||||
type_dict = TypeParser.all_types[ref]
|
||||
|
||||
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
||||
if not type_path or not type_path.exists():
|
||||
raise EVerestParsingException('$ref: ' + ref + f' referenced type file "{type_path}" does not exist.')
|
||||
|
||||
(td, _mod) = TypeParser.load_type_definition(type_path)
|
||||
if 'types' in td and type_dict['type_name'] in td['types']:
|
||||
local_type_info = td['types'][type_dict['type_name']]
|
||||
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
|
||||
prop_info['enum'] = True
|
||||
prop_type = type_dict['namespaced_type']
|
||||
prop_info['prop']['type'] = prop_type
|
||||
prop_info['type_dict'] = type_dict
|
||||
|
||||
path = Path('generated/types') / \
|
||||
type_dict['type_relative_path'].with_suffix('.hpp')
|
||||
type_headers.add(path.as_posix())
|
||||
|
||||
return (prop_type, prop_info)
|
||||
|
||||
|
||||
def parse_property(prop_name: str, prop: Dict, depends_on: List[str], type_file: bool) -> Tuple[str, dict]:
|
||||
"""Determine type of property and proceed with it.
|
||||
In case it is a $ref, look it up in the TypeParser
|
||||
Currently, the following property types are supported:
|
||||
- string (and enum as a special case)
|
||||
- integer
|
||||
- number
|
||||
- boolean
|
||||
- array
|
||||
- object (will be parsed recursivly)
|
||||
"""
|
||||
|
||||
prop_type = None
|
||||
prop_info = {
|
||||
'description': prop.get('description', 'TODO: description'),
|
||||
'prop': prop,
|
||||
'enum': False
|
||||
}
|
||||
if '$ref' in prop:
|
||||
return parse_ref(prop['$ref'], prop_type, prop_info)
|
||||
|
||||
if 'type' not in prop:
|
||||
raise EVerestParsingException(f'{prop_name} does not contain a type property')
|
||||
|
||||
if prop['type'] == 'string':
|
||||
if 'enum' in prop and type_file:
|
||||
prop_type = stringcase.capitalcase(prop_name)
|
||||
add_enum_type(prop_type, prop['enum'], prop_info['description'])
|
||||
elif 'format' in prop:
|
||||
if prop['format'] in format_types:
|
||||
prop_type = format_types[prop['format']]
|
||||
else:
|
||||
# unsupported format type
|
||||
prop_type = 'std::string'
|
||||
prop_info['unsupported_format'] = True
|
||||
else:
|
||||
prop_type = 'std::string'
|
||||
elif prop['type'] == 'integer':
|
||||
prop_type = 'int32_t'
|
||||
elif prop['type'] == 'number':
|
||||
prop_type = 'float'
|
||||
elif prop['type'] == 'boolean':
|
||||
prop_type = 'bool'
|
||||
elif prop['type'] == 'array':
|
||||
if 'items' in prop:
|
||||
prop_type = 'std::vector<' + parse_property(prop_name, prop['items'], depends_on, type_file)[0] + '>'
|
||||
else:
|
||||
raise EVerestParsingException(f'Property items of array {prop_name} does not contain a type property')
|
||||
elif prop['type'] == 'object':
|
||||
prop_type = stringcase.capitalcase(prop_name)
|
||||
depends_on.append(prop_type)
|
||||
if not object_exists(prop_type):
|
||||
parse_object(prop_type, prop, type_file)
|
||||
else:
|
||||
raise Exception('Unknown type: ' + prop['type'])
|
||||
|
||||
return (prop_type, prop_info)
|
||||
|
||||
|
||||
def parse_object(ob_name: str, json_schema: Dict, type_file: bool):
|
||||
"""Parse a JSON 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 'properties' not in json_schema:
|
||||
# object has no properties, probably not a complex object
|
||||
if '$ref' in json_schema:
|
||||
if json_schema['$ref'] not in TypeParser.all_types:
|
||||
TypeParser.all_types[json_schema['$ref']] = TypeParser.parse_type_url(type_url=json_schema['$ref'])
|
||||
type_dict = TypeParser.all_types[json_schema['$ref']]
|
||||
|
||||
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'].with_suffix('.yaml'))
|
||||
if not type_path or not type_path.exists():
|
||||
raise EVerestParsingException(
|
||||
'$ref: ' + json_schema['$ref'] + f' referenced type file "{type_path}" does not exist.')
|
||||
TypeParser.does_type_exist(type_url=json_schema['$ref'], json_type=json_schema['type'])
|
||||
|
||||
prop_type = type_dict['namespaced_type']
|
||||
ob_dict['name'] = prop_type
|
||||
path = Path('generated/types') / \
|
||||
type_dict['type_relative_path'].with_suffix('.hpp')
|
||||
type_headers.add(path.as_posix())
|
||||
return ob_dict
|
||||
return
|
||||
|
||||
if not type_file:
|
||||
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, prop_info) = parse_property(prop_name, prop, ob_dict['depends_on'], type_file)
|
||||
ob_dict['properties'].append({
|
||||
'name': prop_name,
|
||||
'json_name': prop_name,
|
||||
'type': prop_type,
|
||||
'info': prop_info,
|
||||
'enum': 'enum' in prop or prop_info['enum'],
|
||||
'required': prop_name in json_schema.get('required', {}),
|
||||
})
|
||||
|
||||
ob_dict['properties'].sort(key=lambda x: x.get('required'), reverse=True)
|
||||
|
||||
return ob_dict
|
||||
|
||||
|
||||
def generate_header_for_type(type_name: str) -> Path:
|
||||
return (Path('generated/types') / type_name).with_suffix('.hpp')
|
||||
|
||||
|
||||
def extended_build_type_info(name: str, info: dict, type_file=False) -> Tuple[dict, dict]:
|
||||
"""Extend build_type_info with enum and object type handling."""
|
||||
type_info = build_type_info(name, info['type'])
|
||||
enum_info = None
|
||||
|
||||
if type_info['json_type'] == 'string':
|
||||
if 'enum' in info and type_file:
|
||||
enum_info = {
|
||||
'name': name,
|
||||
'description': info.get('description', 'TODO: description'),
|
||||
'enum_type': stringcase.capitalcase(name),
|
||||
'enum': info['enum']
|
||||
}
|
||||
|
||||
type_info['enum_type'] = enum_info['enum_type']
|
||||
elif '$ref' in info:
|
||||
if info['$ref'] not in TypeParser.all_types:
|
||||
TypeParser.all_types[info['$ref']] = TypeParser.parse_type_url(type_url=info['$ref'])
|
||||
type_dict = TypeParser.all_types[info['$ref']]
|
||||
|
||||
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
||||
if not type_path or not type_path.exists():
|
||||
raise EVerestParsingException('$ref: ' + info['$ref'] +
|
||||
f' referenced type file "{type_path}" does not exist.')
|
||||
|
||||
(td, _mod) = TypeParser.load_type_definition(type_path)
|
||||
if 'types' in td and type_dict['type_name'] in td['types']:
|
||||
local_type_info = td['types'][type_dict['type_name']]
|
||||
if local_type_info['type'] == 'string' and 'enum' in local_type_info:
|
||||
enum_info = {
|
||||
'name': name,
|
||||
'description': local_type_info.get('description', 'TODO: description'),
|
||||
'enum_type': type_dict['namespaced_type'],
|
||||
'enum': local_type_info['enum']
|
||||
}
|
||||
|
||||
type_info['enum_type'] = enum_info['enum_type']
|
||||
path = generate_header_for_type(type_dict['type_relative_path'])
|
||||
type_headers.add(path.as_posix())
|
||||
elif type_info['json_type'] == 'object':
|
||||
try:
|
||||
ob = parse_object(name, info, type_file)
|
||||
if ob and 'name' in ob:
|
||||
type_info['object_type'] = ob['name']
|
||||
except EVerestParsingException as e:
|
||||
raise EVerestParsingException(f'Error parsing object {name}: {e}')
|
||||
elif type_info['json_type'] == 'array':
|
||||
if '$ref' in info['items']:
|
||||
if info['items']['$ref'] not in TypeParser.all_types:
|
||||
TypeParser.all_types[info['items']['$ref']] = TypeParser.parse_type_url(type_url=info['items']['$ref'])
|
||||
type_dict = TypeParser.all_types[info['items']['$ref']]
|
||||
|
||||
type_path = resolve_everest_dir_path('types' / type_dict['type_relative_path'] .with_suffix('.yaml'))
|
||||
if not type_path or not type_path.exists():
|
||||
raise EVerestParsingException(
|
||||
'$ref: ' + info['items']['$ref'] + f' referenced type file "{type_path}" does not exist.')
|
||||
|
||||
(td, _mod) = TypeParser.load_type_definition(type_path)
|
||||
if 'types' in td and type_dict['type_name'] in td['types']:
|
||||
local_type_info = td['types'][type_dict['type_name']]
|
||||
if 'enum' in local_type_info:
|
||||
type_info['array_type_contains_enum'] = True
|
||||
type_info['array_type'] = type_dict['namespaced_type']
|
||||
path = generate_header_for_type(type_dict['type_relative_path'])
|
||||
type_headers.add(path.as_posix())
|
||||
|
||||
return (type_info, enum_info)
|
||||
|
||||
|
||||
def load_validators(schema_path: Path):
|
||||
# FIXME (aw): we should also patch the schemas like in everest-framework
|
||||
validators = {}
|
||||
for validator, filename in zip(
|
||||
['interface', 'module', 'config', 'type', 'error_declaration_list'],
|
||||
['interface', 'manifest', 'config', 'type', 'error-declaration-list']):
|
||||
try:
|
||||
schema = yaml.safe_load((schema_path / f'{filename}.yaml').read_text())
|
||||
jsonschema.Draft7Validator.check_schema(schema)
|
||||
validators[validator] = jsonschema.Draft7Validator(schema)
|
||||
except OSError as err:
|
||||
print(f'Could not open schema file {err.filename}: {err.strerror}')
|
||||
exit(1)
|
||||
except jsonschema.SchemaError as err:
|
||||
print(f'Schema error in schema file {filename}.yaml')
|
||||
raise
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse interface definition file {schema_path}') from err
|
||||
|
||||
return validators
|
||||
|
||||
|
||||
def load_validated_interface_def(if_def_path: Path, validator):
|
||||
if_def = {}
|
||||
try:
|
||||
if_def = yaml.safe_load(if_def_path.read_text())
|
||||
# validating interface
|
||||
validator.validate(if_def)
|
||||
# validate var/cmd subparts
|
||||
if 'vars' in if_def:
|
||||
for _var_name, var_def in if_def['vars'].items():
|
||||
jsonschema.Draft7Validator.check_schema(var_def)
|
||||
if 'cmds' in if_def:
|
||||
for _cmd_name, cmd_def in if_def['cmds'].items():
|
||||
if 'arguments' in cmd_def:
|
||||
for _arg_name, arg_def in cmd_def['arguments'].items():
|
||||
jsonschema.Draft7Validator.check_schema(arg_def)
|
||||
if 'result' in cmd_def:
|
||||
jsonschema.Draft7Validator.check_schema(cmd_def['result'])
|
||||
except OSError as err:
|
||||
raise Exception(f'Could not open interface definition file {err.filename}: {err.strerror}') from err
|
||||
except jsonschema.ValidationError as err:
|
||||
raise Exception(f'Validation error in interface definition file {if_def_path}: {err}') from err
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse interface definition file {if_def_path}') from err
|
||||
|
||||
return if_def
|
||||
|
||||
|
||||
def load_validated_type_def(type_def_path: Path, validator):
|
||||
"""Load a type definition from the provided path and validate it with the provided validator."""
|
||||
|
||||
try:
|
||||
type_def = yaml.safe_load(type_def_path.read_text())
|
||||
# validating type definition
|
||||
validator.validate(type_def)
|
||||
|
||||
return type_def
|
||||
except OSError as err:
|
||||
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
|
||||
except jsonschema.ValidationError as err:
|
||||
raise Exception(f'Validation error in type definition file {type_def_path}') from err
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse interface definition file {type_def_path}') from err
|
||||
|
||||
return type_def
|
||||
|
||||
|
||||
def load_validated_module_def(module_path: Path, validator):
|
||||
try:
|
||||
module_def = yaml.safe_load(module_path.read_text())
|
||||
validator.validate(module_def)
|
||||
except OSError as err:
|
||||
raise Exception(f'Could not open type definition file {err.filename}: {err.strerror}') from err
|
||||
except jsonschema.ValidationError as err:
|
||||
raise Exception(f'Validation error in module definition file {module_path}') from err
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse interface definition file {module_path}') from err
|
||||
|
||||
return module_def
|
||||
|
||||
|
||||
def generate_some_uuids(count):
|
||||
for i in range(count):
|
||||
print(uuid4())
|
||||
|
||||
|
||||
def yaml2json(yaml_file: Path, json_file: Path):
|
||||
if not yaml_file.exists():
|
||||
print(f'The input file ({yaml_file}) does not exist')
|
||||
exit(1)
|
||||
|
||||
with open(yaml_file, 'r') as yaml_content:
|
||||
content_as_dict = yaml.safe_load(yaml_content)
|
||||
|
||||
with open(json_file, 'w') as json_content:
|
||||
json.dump(content_as_dict, json_content, indent=2)
|
||||
|
||||
|
||||
def json2yaml(json_file: Path, yaml_file: Path):
|
||||
if not json_file.exists():
|
||||
print(f'The input file ({json_file}) does not exist')
|
||||
exit(1)
|
||||
|
||||
with open(json_file, 'r') as json_content:
|
||||
content_as_dict = json.load(json_content)
|
||||
|
||||
with open(yaml_file, 'w') as yaml_content:
|
||||
yaml.safe_dump(content_as_dict, yaml_content, indent=2, sort_keys=False, width=120)
|
||||
|
||||
|
||||
def __check_for_match(blocks_def, line, line_no, file_path):
|
||||
match = re.search(blocks_def['regex_str'], line)
|
||||
if not match:
|
||||
return None
|
||||
|
||||
# mb = match_block
|
||||
mb = {
|
||||
'id': match.group('uuid'),
|
||||
'version': match.group('version'),
|
||||
'tag': blocks_def['format_str'].format(
|
||||
uuid=match.group('uuid'),
|
||||
version=match.group('version')
|
||||
)
|
||||
}
|
||||
|
||||
# check if uuid and version exists
|
||||
if blocks_def['version'] != mb['version']:
|
||||
raise ValueError(
|
||||
f'Error while parsing {file_path}:\n'
|
||||
f' matched line {line_no}: {line}\n'
|
||||
f' contains version "{mb["version"]}", which is different from the blocks definition version "{blocks_def["version"]}"'
|
||||
)
|
||||
|
||||
for block, block_info in blocks_def['definitions'].items():
|
||||
if block_info['id'] != mb['id']:
|
||||
continue
|
||||
|
||||
mb['name'] = block
|
||||
mb['block'] = block_info
|
||||
|
||||
if not 'block' in mb:
|
||||
raise ValueError(
|
||||
f'Error while parsing {file_path}:\n'
|
||||
f' matched line {line_no}: {line}\n'
|
||||
f' contains uuid "{mb["id"]}", which doesn\'t exist in the block definition'
|
||||
)
|
||||
|
||||
return mb
|
||||
|
||||
|
||||
def generate_tmpl_blocks(blocks_def, file_path=None):
|
||||
|
||||
tmpl_block = {}
|
||||
for block_name, block_def in blocks_def['definitions'].items():
|
||||
tmpl_block[block_name] = {
|
||||
'tag': blocks_def['format_str'].format(
|
||||
uuid=block_def['id'],
|
||||
version=blocks_def['version']
|
||||
),
|
||||
'content': block_def['content'],
|
||||
'first_use': True
|
||||
}
|
||||
|
||||
if not file_path:
|
||||
return tmpl_block
|
||||
|
||||
try:
|
||||
file_data = file_path.read_text()
|
||||
except OSError as err:
|
||||
print(f'Could not open file {err.filename} for parsing blocks: {err.strerror}')
|
||||
exit(1)
|
||||
|
||||
line_no = 0
|
||||
matched_block = None
|
||||
content = None
|
||||
|
||||
for line in file_data.splitlines(True):
|
||||
line_no += 1
|
||||
|
||||
if not matched_block:
|
||||
matched_block = __check_for_match(blocks_def, line.rstrip(), line_no, file_path)
|
||||
content = None
|
||||
continue
|
||||
|
||||
if (line.strip() == matched_block['tag']):
|
||||
if (content):
|
||||
tmpl_block[matched_block['name']]['content'] = content.rstrip()
|
||||
tmpl_block[matched_block['name']]['first_use'] = False
|
||||
matched_block = None
|
||||
else:
|
||||
content = (content + line) if content else line
|
||||
|
||||
if matched_block:
|
||||
raise ValueError(
|
||||
f'Error while parsing {file_path}:\n'
|
||||
f' matched tag line {matched_block["tag"]}\n'
|
||||
f' could not find closing tag'
|
||||
)
|
||||
|
||||
return tmpl_block
|
||||
|
||||
|
||||
def load_tmpl_blocks(blocks_def, file_path, update):
|
||||
if update and file_path.exists():
|
||||
return generate_tmpl_blocks(blocks_def, file_path)
|
||||
else:
|
||||
return generate_tmpl_blocks(blocks_def)
|
||||
|
||||
|
||||
def __show_diff_for(file_info):
|
||||
diff_path = shutil.which('diff')
|
||||
if diff_path == None:
|
||||
raise Exception('Can\'t generate diff, because "diff" executable not found')
|
||||
|
||||
file_path = file_info['path']
|
||||
|
||||
diff_ignore = ''
|
||||
if file_path.suffix in ('.hpp', '.cpp'):
|
||||
diff_ignore = '^//.*'
|
||||
elif file_path.name == 'CMakeLists.txt':
|
||||
diff_ignore = '^#.*'
|
||||
diff_ignore_args = ['-I', diff_ignore] if diff_ignore else []
|
||||
|
||||
run_parms = {'input': file_info['content'], 'capture_output': True, 'encoding': 'utf-8'}
|
||||
|
||||
diff = subprocess.run([
|
||||
diff_path,
|
||||
'-ruN',
|
||||
*diff_ignore_args,
|
||||
'--label', file_info['printable_name'],
|
||||
'--color=always',
|
||||
file_path,
|
||||
'-'
|
||||
], **run_parms).stdout
|
||||
if diff:
|
||||
print(diff)
|
||||
|
||||
|
||||
def filter_mod_files(only, mod_files):
|
||||
if not only:
|
||||
return
|
||||
|
||||
filter_files = set([c.strip() for c in only.split(',')])
|
||||
not_filtered_files = filter_files.copy()
|
||||
|
||||
# first check if all selected file filters are valid
|
||||
for category_files in mod_files.values():
|
||||
for file_info in category_files:
|
||||
if file_info['abbr'] in not_filtered_files:
|
||||
not_filtered_files.remove(file_info['abbr'])
|
||||
|
||||
if not_filtered_files:
|
||||
raise Exception(f'Unknown file filters for --only option: {not_filtered_files}\n'
|
||||
'Use "--only which" to show available file filters')
|
||||
|
||||
# now do the filtering
|
||||
for category, category_files in mod_files.items():
|
||||
mod_files[category] = list(filter(lambda x: x['abbr'] in filter_files, category_files))
|
||||
|
||||
|
||||
def print_available_mod_files(mod_files):
|
||||
for category, category_files in mod_files.items():
|
||||
print(f'Available files for category "{category}"')
|
||||
for file_info in category_files:
|
||||
print(f' {file_info["abbr"]}')
|
||||
|
||||
def get_mtime(filename: str | Path) -> float:
|
||||
if isinstance(filename, str):
|
||||
filename = Path(filename)
|
||||
|
||||
return filename.stat().st_mtime
|
||||
|
||||
|
||||
def is_template_newer(file_info) -> Tuple[bool, str]:
|
||||
template_path = file_info['template_path']
|
||||
generated_path = file_info['path']
|
||||
|
||||
if not generated_path.exists():
|
||||
return (True, ' (Generated file did not exist)')
|
||||
|
||||
if get_mtime(template_path) > get_mtime(generated_path):
|
||||
return (True, ' (Template file has changed since last generation)')
|
||||
|
||||
return (False, '')
|
||||
|
||||
|
||||
def write_content_to_file(file_info, strategy, only_diff=False, reason = '', check_license_header=False):
|
||||
# strategy:
|
||||
# update: update only if dest older or not existent
|
||||
# force-update: update, even if dest newer
|
||||
# update-if-non-existent: update only if file does not exists
|
||||
# create: create file only if it does not exist
|
||||
# force-create: create file, even if it exists
|
||||
# FIXME (aw): we should have this as an enum
|
||||
|
||||
strategies = ['update', 'force-update', 'update-if-non-existent', 'create', 'force-create']
|
||||
|
||||
file_path = file_info['path']
|
||||
file_dir = file_path.parent
|
||||
printable_name = file_info['printable_name']
|
||||
|
||||
method = ''
|
||||
|
||||
if only_diff:
|
||||
return __show_diff_for(file_info)
|
||||
|
||||
if strategy == 'update':
|
||||
if file_path.exists() and file_path.stat().st_mtime > file_info['last_mtime']:
|
||||
print(f'Skipping {printable_name} (up-to-date)')
|
||||
return
|
||||
method = 'Updating'
|
||||
elif strategy == 'force-update':
|
||||
method = 'Force-updating' if file_path.exists() else 'Creating'
|
||||
elif strategy == 'force-create':
|
||||
method = 'Overwriting' if file_path.exists() else 'Creating'
|
||||
elif strategy == 'update-if-non-existent' or strategy == 'create':
|
||||
if file_path.exists():
|
||||
print(f'Skipping {printable_name} (use create --force to recreate)')
|
||||
return
|
||||
method = 'Creating'
|
||||
else:
|
||||
raise Exception(f'Invalid strategy "{strategy}"\nSupported strategies: {strategies}')
|
||||
|
||||
print(f'{method} file {printable_name}{reason}')
|
||||
|
||||
if not file_dir.exists():
|
||||
file_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# check if file header is different from license header
|
||||
if check_license_header:
|
||||
if 'license_header' in file_info and file_path.exists():
|
||||
original_content = file_path.read_text()
|
||||
if not original_content.startswith(file_info['license_header']):
|
||||
# determine likely end of license header
|
||||
search_terms = ['#ifndef', '#pragma once', '#include']
|
||||
original_license_header = ''
|
||||
for search in search_terms:
|
||||
index = original_content.find(search)
|
||||
if index >= 0:
|
||||
original_license_header = original_content[0:index]
|
||||
break
|
||||
print(f'Keeping the existing licence header:\n{original_license_header}')
|
||||
file_info['content'] = file_info['content'].replace(
|
||||
file_info['license_header'], original_license_header.strip())
|
||||
|
||||
file_path.write_text(file_info['content'])
|
||||
|
||||
|
||||
def write_content_to_file_and_check_template(file_info, strategy, only_diff=False):
|
||||
# check if template is newer and force-update file if it is
|
||||
update_strategy = strategy
|
||||
(newer, reason) = is_template_newer(file_info)
|
||||
if newer:
|
||||
update_strategy = 'force-update'
|
||||
write_content_to_file(file_info, update_strategy, only_diff, reason)
|
||||
|
||||
|
||||
def get_license_header(license_dirs, license_url):
|
||||
url_schemas = ['http://', 'https://']
|
||||
for url_schema in url_schemas:
|
||||
if license_url.startswith(url_schema):
|
||||
license_url = license_url.replace(url_schema, '', 1)
|
||||
license_path = None
|
||||
for license_dir in license_dirs:
|
||||
check_license_path = license_dir / license_url
|
||||
print(f'Checking if license "{check_license_path}" exists...')
|
||||
if check_license_path.exists():
|
||||
license_path = check_license_path
|
||||
|
||||
if not license_path:
|
||||
return None
|
||||
with open(license_path, 'r') as custom_license_file:
|
||||
return custom_license_file.read().strip()
|
||||
|
||||
|
||||
def get_path_from_cmake_cache(variable_prefix, cmake_cache_path, option_name):
|
||||
print(f'Searching for {variable_prefix} in: {cmake_cache_path}')
|
||||
print(f'You can either provide the {variable_prefix} directory with {option_name} or influence the'
|
||||
' automatic search path by setting --build-dir (default: ./build)')
|
||||
if not cmake_cache_path.exists():
|
||||
print(f'CMakeCache.txt does not exist: {cmake_cache_path}')
|
||||
return None
|
||||
with open(cmake_cache_path, 'r') as cmake_cache_file:
|
||||
search = f'{variable_prefix}_SOURCE_DIR:STATIC='
|
||||
for line in cmake_cache_file:
|
||||
if line.startswith(search):
|
||||
found_dir = Path(line.replace(search, '', 1).strip(' \t\n\r'))
|
||||
if found_dir.exists():
|
||||
print(f'Found {variable_prefix} directory: {found_dir}')
|
||||
user_choice = input('Do you want to use this? [Y/n] ').lower()
|
||||
if user_choice == 'y' or not user_choice:
|
||||
return found_dir
|
||||
break
|
||||
return None
|
||||
|
||||
|
||||
def detect_everest_projects(everest_projects, build_dir):
|
||||
detected_everest_project = False
|
||||
for everest_dir in everest_dirs:
|
||||
if everest_dir.exists() and everest_dir.name in everest_projects:
|
||||
detected_everest_project = True
|
||||
|
||||
found_dirs = []
|
||||
|
||||
if not detected_everest_project:
|
||||
print('Could not detect ' + ", ".join(everest_projects) + ' path in --everest-dir')
|
||||
cmake_cache_path = Path(build_dir) / 'CMakeCache.txt'
|
||||
for everest_project in everest_projects:
|
||||
found_dir = get_path_from_cmake_cache(everest_project, cmake_cache_path, '--everest-dir')
|
||||
if found_dir:
|
||||
found_dirs.append(found_dir)
|
||||
|
||||
return found_dirs
|
||||
@@ -0,0 +1,2 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
@@ -0,0 +1,2 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
@@ -0,0 +1,2 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
@@ -0,0 +1,19 @@
|
||||
{% from "helper_macros.j2" import insert_block, print_template_info %}
|
||||
{{ print_template_info('3', 'marked regions will be kept', '#') }}
|
||||
|
||||
# module setup:
|
||||
# - ${MODULE_NAME}: module name
|
||||
ev_setup_cpp_module()
|
||||
|
||||
{{ insert_block(info.blocks.add_general) }}
|
||||
|
||||
{% if provides %}
|
||||
target_sources(${MODULE_NAME}
|
||||
PRIVATE
|
||||
{% for impl in provides %}
|
||||
"{{ impl.cpp_file_rel_path }}"
|
||||
{% endfor %}
|
||||
)
|
||||
|
||||
{% endif %}
|
||||
{{ insert_block(info.blocks.add_other) }}
|
||||
@@ -0,0 +1,127 @@
|
||||
{% macro cpp_type(type, interface=none, call=false) -%}
|
||||
{% if type.is_variant -%}
|
||||
std::variant<{{ type.cpp_type|join(', ') }}>
|
||||
{%- else -%}
|
||||
{% if interface is not none and 'object_type' in type %}
|
||||
{{ interface + '::' + type.object_type }}
|
||||
{% else %}
|
||||
{% if call -%}{{'const '}}{%- endif %}
|
||||
{% if 'object_type' in type -%}
|
||||
{{ type.object_type }}
|
||||
{%- elif 'enum_type' in type -%}
|
||||
{{ type.enum_type }}
|
||||
{%- elif 'array_type' in type -%}
|
||||
std::vector<{{ type.array_type }}>
|
||||
{%- else -%}
|
||||
{{ type.cpp_type }}
|
||||
{%- endif %}
|
||||
{% if call -%}&{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro result_type(type, interface=none) -%}
|
||||
{% if type -%}
|
||||
{{ cpp_type(type) }}
|
||||
{%- else -%}
|
||||
void
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro var_to_any(var, name) -%}
|
||||
{% if var.is_variant -%}
|
||||
Everest::variant_to_json({{ name }})
|
||||
{%- else -%}
|
||||
{{ name }}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro var_to_cpp(arg) -%}
|
||||
{% if arg.is_variant -%}
|
||||
Everest::json_to_variant<{{ arg.cpp_type|join(', ')}}>
|
||||
{%- else -%}
|
||||
static_cast<{{ arg.cpp_type }}>
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro call_cmd_signature(cmd, interface=none) -%}
|
||||
{{ result_type(cmd.result, interface) }} call_{{ cmd.name }}(
|
||||
{%- for arg in cmd.args -%}
|
||||
{{ cpp_type(arg, none, true) }} {{ arg.name }}{{ ', ' if not loop.last }}
|
||||
{%- endfor -%}
|
||||
)
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro handle_cmd_signature(cmd, class_name=None, interface=none) -%}
|
||||
{% if not class_name %}virtual {% endif -%}
|
||||
{{ result_type(cmd.result, interface) }} {% if class_name %}{{ class_name }}::{% endif -%}
|
||||
handle_{{ cmd.name }}(
|
||||
{%- for arg in cmd.args -%}
|
||||
{{ cpp_type(arg) }}& {{ arg.name }}{{ ', ' if not loop.last }}
|
||||
{%- endfor -%}
|
||||
)
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro publish_var_signature(var) %}
|
||||
void publish_{{ var.name }}({% if var.json_type != 'null' %}{{ cpp_type(var) }} value{% endif %}) {
|
||||
_ev->publish(_name, "{{ var.name }}", {% if var.json_type != 'null' %}{{ var_to_any(var, 'value')}}{% else %}nullptr{% endif %});
|
||||
}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro list_json_types(json_type) -%}
|
||||
{% if json_type is iterable and json_type is not string -%}
|
||||
{
|
||||
{%- for type in json_type -%}
|
||||
"{{ type }}"{{ ', ' if not loop.last}}
|
||||
{%- endfor -%}
|
||||
}
|
||||
{%- else -%}
|
||||
{"{{ json_type }}"}
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro insert_block(block, indent=0) %}
|
||||
{{ block.tag }}
|
||||
{{ " "*indent if block.first_use }}{{ block.content }}
|
||||
{{ " "*indent }}{{ block.tag }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro print_spdx_line(license, year_tag=None) %}
|
||||
// SPDX-License-Identifier: {{ license }}
|
||||
// Copyright{% if year_tag %} {{ year_tag }}{% endif %} Pionix GmbH and Contributors to EVerest
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro print_license_header(license) %}
|
||||
{{ license }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro print_template_info(version, title='DO NOT EDIT!', comment_sep='//') %}
|
||||
{{ comment_sep }}
|
||||
{{ comment_sep }} AUTO GENERATED - {{ title|upper }}
|
||||
{{ comment_sep }} template version {{ version }}
|
||||
{{ comment_sep }}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro string_to_enum(enum_type) %}
|
||||
{% if '::' not in enum_type %}
|
||||
string_to_{{ enum_type | snake_case -}}
|
||||
{% else %}
|
||||
{{ enum_type[:enum_type.rfind('::')] }}::string_to_{{ enum_type[(enum_type.rfind('::')+2):] | snake_case -}}
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro enum_to_string(enum_type) %}
|
||||
{% if '::' not in enum_type %}
|
||||
{{ enum_type | snake_case }}_to_string
|
||||
{%- else %}
|
||||
{{ enum_type[:enum_type.rfind('::')] }}::{{ enum_type[(enum_type.rfind('::')+2):] | snake_case }}_to_string
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro enum_to_string_view(enum_type) %}
|
||||
{% if '::' not in enum_type %}
|
||||
{{ enum_type | snake_case }}_to_string_view
|
||||
{%- else %}
|
||||
{{ enum_type[:enum_type.rfind('::')] }}::{{ enum_type[(enum_type.rfind('::')+2):] | snake_case }}_to_string_view
|
||||
{%- endif %}
|
||||
{%- endmacro %}
|
||||
@@ -0,0 +1,24 @@
|
||||
.. _everest_modules_handwritten_{{ info.name }}:
|
||||
|
||||
.. This file is a placeholder for optional multiple files
|
||||
handwritten documentation for the {{ info.name }} module.
|
||||
|
||||
.. This handwritten documentation is optional. In case
|
||||
you do not want to write it, you can delete the doc/ directory.
|
||||
|
||||
.. The documentation can be written in reStructuredText,
|
||||
and will be converted to HTML and PDF by Sphinx.
|
||||
This index.rst file is the entry point for the module documentation.
|
||||
|
||||
.. Use underlined-only headlines inside this document (highest-level
|
||||
sub-section headline should use "=" characters)
|
||||
|
||||
.. The content of this file will be included in the auto-generated HTML
|
||||
page for the module. You can link to it using the following
|
||||
reference: everest_modules_{{ info.name }}.
|
||||
|
||||
.. *******************************************
|
||||
.. {{ info.name }}
|
||||
.. *******************************************
|
||||
|
||||
{{ info.desc }}
|
||||
@@ -0,0 +1,194 @@
|
||||
{% from "helper_macros.j2" import handle_cmd_signature, publish_var_signature, list_json_types, var_to_cpp, var_to_any, print_template_info, print_spdx_line, string_to_enum, enum_to_string, enum_to_string_view %}
|
||||
{{ print_spdx_line('Apache-2.0') }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('6') }}
|
||||
|
||||
#include <framework/ModuleAdapter.hpp>
|
||||
#include <utils/types.hpp>
|
||||
#include <utils/error.hpp>
|
||||
#include <utils/error/error_state_monitor.hpp>
|
||||
#include <utils/error/error_manager_impl.hpp>
|
||||
#include <utils/error/error_factory.hpp>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
class {{ info.class_name }} : public Everest::ImplementationBase {
|
||||
public:
|
||||
{{ info.class_name }}(Everest::ModuleAdapter* ev, const std::string& name)
|
||||
: Everest::ImplementationBase(),
|
||||
_ev(ev),
|
||||
_name(name) {
|
||||
if (ev == nullptr) {
|
||||
EVLOG_error << "ev is nullptr, please check the initialization of the module";
|
||||
error_manager = nullptr;
|
||||
error_state_monitor = nullptr;
|
||||
error_factory = nullptr;
|
||||
EVLOG_error << "error_manager, error_state_monitor and error_factory are nullptr";
|
||||
} else {
|
||||
error_manager = ev->get_error_manager_impl(name);
|
||||
if (error_manager == nullptr) {
|
||||
EVLOG_error << "error_manager is nullptr";
|
||||
}
|
||||
error_state_monitor = ev->get_error_state_monitor_impl(name);
|
||||
if (error_state_monitor == nullptr) {
|
||||
EVLOG_error << "error_state_monitor is nullptr";
|
||||
}
|
||||
error_factory = ev->get_error_factory(name);
|
||||
if (error_factory == nullptr) {
|
||||
EVLOG_error << "error_factory is nullptr";
|
||||
}
|
||||
impl_mapping = Everest::get_impl_mapping(ev->get_mapping(), name);
|
||||
}
|
||||
}
|
||||
|
||||
{% if not vars %}
|
||||
// no variables defined for this interface
|
||||
{% else %}
|
||||
// publish functions for variables
|
||||
{% for var in vars %}
|
||||
void publish_{{ var.name }}(
|
||||
{%- if 'array_type' in var %}
|
||||
const std::vector<{{ var.array_type }}>&
|
||||
{%- elif 'object_type' in var %}
|
||||
{{- 'const '}} {{- var.object_type }}&
|
||||
{%- elif 'enum_type' in var %}
|
||||
{{- 'const '}} {{- var.enum_type }}&
|
||||
{%- else %}
|
||||
{{- 'const '}} {{- var.cpp_type }}&
|
||||
{%- endif -%}
|
||||
{{ ' value) {' }}
|
||||
{% if 'object_type' in var %}
|
||||
_ev->publish(_name, "{{ var.name }}", value);
|
||||
{% elif 'enum_type' in var %}
|
||||
_ev->publish(_name, "{{ var.name }}", {{ enum_to_string_view(var.enum_type) }}(value));
|
||||
{% elif 'array_type' in var %}
|
||||
{% if 'array_type_contains_enum' in var %}
|
||||
std::vector<std::string> string_array;
|
||||
string_array.reserve(value.size());
|
||||
for (const auto& entry : value) {
|
||||
string_array.push_back({{ enum_to_string(var.array_type) }}(entry));
|
||||
}
|
||||
_ev->publish(_name, "{{ var.name }}", string_array);
|
||||
{% else %}
|
||||
_ev->publish(_name, "{{ var.name }}", value);
|
||||
{% endif %}
|
||||
{% else %}
|
||||
_ev->publish(_name, "{{ var.name }}", value);
|
||||
{% endif %}
|
||||
}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
void raise_error(const Everest::error::Error& error) {
|
||||
error_manager->raise_error(error);
|
||||
}
|
||||
|
||||
void clear_error(const Everest::error::ErrorType& type) {
|
||||
error_manager->clear_error(type);
|
||||
}
|
||||
|
||||
void clear_error(const Everest::error::ErrorType& type, const Everest::error::ErrorSubType& sub_type) {
|
||||
error_manager->clear_error(type, sub_type);
|
||||
}
|
||||
|
||||
void clear_all_errors_of_impl() {
|
||||
error_manager->clear_all_errors();
|
||||
}
|
||||
|
||||
void clear_all_errors_of_impl(const Everest::error::ErrorType& type) {
|
||||
error_manager->clear_all_errors(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor> error_state_monitor;
|
||||
std::shared_ptr<Everest::error::ErrorFactory> error_factory;
|
||||
std::shared_ptr<Everest::error::ErrorManagerImpl> error_manager;
|
||||
|
||||
std::optional<Mapping> get_mapping() {
|
||||
return impl_mapping;
|
||||
}
|
||||
|
||||
protected:
|
||||
{% if not cmds %}
|
||||
// no commands defined for this interface
|
||||
{% else %}
|
||||
// command handler functions (virtual)
|
||||
{% for cmd in cmds %}
|
||||
{{ handle_cmd_signature(cmd, none, info.interface_name) }} = 0;
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
private:
|
||||
Everest::ModuleAdapter* const _ev;
|
||||
const std::string _name;
|
||||
std::optional<Mapping> impl_mapping;
|
||||
|
||||
// helper function for getting all commands
|
||||
void _gather_cmds([[maybe_unused]] std::vector<Everest::cmd>& cmds) override {
|
||||
{% if not cmds %}
|
||||
// this interface does not offer any commands
|
||||
{% else %}
|
||||
{% for cmd in cmds %}
|
||||
// {{ cmd.name }} command
|
||||
Everest::cmd {{ cmd.name }}_cmd;
|
||||
{{ cmd.name }}_cmd.impl_id = _name;
|
||||
{{ cmd.name }}_cmd.cmd_name = "{{ cmd.name }}";
|
||||
{% if not cmd.args %}
|
||||
// cmd {{ cmd.name }} has no arguments
|
||||
{% else %}
|
||||
{{ cmd.name }}_cmd.arg_types = {
|
||||
{% for arg in cmd.args %}
|
||||
{"{{ arg.name }}", {{ list_json_types(arg.json_type) + '}' }}{{ ',' if not loop.last }}
|
||||
{% endfor %}
|
||||
};
|
||||
{% endif %}
|
||||
{{ cmd.name }}_cmd.cmd = [this](const Parameters& args) -> Result {
|
||||
{% for arg in cmd.args %}
|
||||
{% if 'object_type' in arg %}
|
||||
auto {{ arg.name }} = args.at("{{ arg.name }}").get<{{ arg.object_type }}>();
|
||||
{% elif 'enum_type' in arg %}
|
||||
auto {{ arg.name }} = {{ string_to_enum(arg.enum_type) }}(args.at("{{ arg.name }}").get<std::string>());
|
||||
{% elif 'array_type' in arg %}
|
||||
json {{ arg.name }}_json_array = args["{{ arg.name }}"];
|
||||
{% if 'array_type_contains_enum' in arg %}
|
||||
std::vector<{{ arg.array_type }}> {{ arg.name }};
|
||||
for (auto entry : {{ arg.name }}_json_array) {
|
||||
{{ arg.name }}.push_back({{ string_to_enum(arg.array_type) }}(entry));
|
||||
}
|
||||
{% else %}
|
||||
auto {{ arg.name }} = args.at("{{ arg.name }}").get<std::vector<{{ arg.array_type }}>>();
|
||||
{% endif %}
|
||||
{% else %}
|
||||
auto {{ arg.name }} = {{ var_to_cpp(arg) }}(args.at("{{ arg.name }}"));
|
||||
{% endif %}
|
||||
{% else %}
|
||||
(void) args; // no arguments used for this callback
|
||||
{% endfor %}
|
||||
|
||||
{{ 'auto result = ' if cmd.result }}this->handle_{{ cmd.name }}(
|
||||
{%- for arg in cmd.args -%}
|
||||
{{ arg.name }}{{ ', ' if not loop.last }}
|
||||
{%- endfor -%}
|
||||
);
|
||||
{% if cmd.result and 'object_type' in cmd.result %}
|
||||
return result;
|
||||
{% elif cmd.result and 'enum_type' in cmd.result %}
|
||||
return {{ enum_to_string(cmd.result.enum_type) }}(result);
|
||||
{% else %}
|
||||
return {{ var_to_any(cmd.result, 'result') if cmd.result else 'nullptr'}};
|
||||
{% endif %}
|
||||
};
|
||||
{% if cmd.result %}
|
||||
{{ cmd.name }}_cmd.return_type = {{ list_json_types(cmd.result.json_type) }};
|
||||
{% endif %}
|
||||
cmds.emplace_back(std::move({{ cmd.name }}_cmd));
|
||||
{% if not loop.last %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
};
|
||||
};
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,163 @@
|
||||
{% from "helper_macros.j2" import call_cmd_signature, var_to_any, var_to_cpp, print_template_info, cpp_type, result_type, print_spdx_line, string_to_enum, enum_to_string %}
|
||||
{{ print_spdx_line('Apache-2.0') }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('5') }}
|
||||
|
||||
#include <framework/ModuleAdapter.hpp>
|
||||
#include <utils/types.hpp>
|
||||
#include <utils/error.hpp>
|
||||
#include <utils/error/error_state_monitor.hpp>
|
||||
#include <utils/error/error_manager_req.hpp>
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
class {{ info.class_name }} {
|
||||
public:
|
||||
{{ info.class_name }}(Everest::ModuleAdapter* adapter, Requirement req, const std::string& module_id, std::optional<Mapping> mapping)
|
||||
: module_id(module_id),
|
||||
_adapter(adapter),
|
||||
_req(req),
|
||||
_mapping(mapping) {
|
||||
if (adapter == nullptr) {
|
||||
EVLOG_error << "adapter is nullptr, please check the initialization of the module";
|
||||
error_manager = nullptr;
|
||||
error_state_monitor = nullptr;
|
||||
EVLOG_error << "error_manager and error_state_monitor are nullptr";
|
||||
} else {
|
||||
error_manager = adapter->get_error_manager_req(req);
|
||||
if (error_manager == nullptr) {
|
||||
EVLOG_error << "error_manager is nullptr";
|
||||
}
|
||||
error_state_monitor = adapter->get_error_state_monitor_req(req);
|
||||
if (error_state_monitor == nullptr) {
|
||||
EVLOG_error << "error_state_monitor is nullptr";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string module_id;
|
||||
|
||||
{% if not vars %}
|
||||
// this interface does not export any variables to subscribe to
|
||||
{% else %}
|
||||
// variables available for subscription
|
||||
{% for var in vars %}
|
||||
void subscribe_{{ var.name }}(const std::function<void({% if var.json_type != 'null' %}const {{ cpp_type(var) }}&{% endif %})>& listener) {
|
||||
_adapter->subscribe(_req, "{{ var.name }}", [func = std::move(listener)](const Value& value) {
|
||||
{% if 'object_type' in var %}
|
||||
func(value);
|
||||
{% elif 'enum_type' in var %}
|
||||
func({{ string_to_enum(var.enum_type) }}({{ var_to_cpp(var) }}(value)));
|
||||
{% elif 'array_type' in var %}
|
||||
{% if 'array_type_contains_enum' in var %}
|
||||
std::vector<{{ var.array_type }}> typed_value;
|
||||
for (auto& entry : value) {
|
||||
typed_value.push_back({{ string_to_enum(var.array_type) }}(entry));
|
||||
}
|
||||
func(typed_value);
|
||||
{% else %}
|
||||
func(value);
|
||||
{% endif %}
|
||||
{% elif var.json_type != 'null' %}
|
||||
func({{ var_to_cpp(var) }}(value));
|
||||
{% else %}
|
||||
if (not Everest::detail::is_type_compatible<{{ cpp_type(var) }}>(value.type())) {
|
||||
EVLOG_error << "Callback for variable '{{ var.name }}' in interface '{{ info.interface }}' has wrong type!";
|
||||
}
|
||||
func();
|
||||
{% endif %}
|
||||
});
|
||||
}
|
||||
{% if not loop.last %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
void subscribe_error(
|
||||
const Everest::error::ErrorType& type,
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback
|
||||
) {
|
||||
error_manager->subscribe_error(type, callback, clear_callback);
|
||||
}
|
||||
|
||||
void subscribe_all_errors(
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback
|
||||
) {
|
||||
error_manager->subscribe_all_errors(callback, clear_callback);
|
||||
}
|
||||
|
||||
{% if not cmds %}
|
||||
// this interface does not export any commands to call
|
||||
{% else %}
|
||||
// commands available to call
|
||||
{% for cmd in cmds %}
|
||||
{{ call_cmd_signature(cmd, info.interface_name) }} {
|
||||
{% for arg in cmd.args %}
|
||||
{% if 'array_type' in arg %}
|
||||
{% if 'array_type_contains_enum' in arg %}
|
||||
Array {{ arg.name }}_array;
|
||||
for (const auto& {{ arg.name }}_entry : {{ arg.name }}) {
|
||||
{{ arg.name }}_array.push_back({{ enum_to_string(arg.array_type) }}({{ arg.name }}_entry));
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{ '' }}{% if cmd.result %}Result result = {% endif %}_adapter->call(_req, "{{ cmd.name }}",
|
||||
{{ 'Parameters{' }}
|
||||
{% for arg in cmd.args %}
|
||||
{% if 'enum_type' in arg %}
|
||||
{"{{ arg.name }}", {{ enum_to_string(arg.enum_type) }}({{ arg.name }})}
|
||||
{% elif 'object_type' in arg %}
|
||||
{"{{ arg.name }}", {{ arg.name }}}
|
||||
{% elif 'array_type' in arg %}
|
||||
{% if 'array_type_contains_enum' in arg %}
|
||||
{"{{ arg.name }}", {{ var_to_any(arg, arg.name + '_array') }}}
|
||||
{% else %}
|
||||
{"{{ arg.name }}", {{ var_to_any(arg, arg.name) }}}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{"{{ arg.name }}", {{ var_to_any(arg, arg.name) }}}
|
||||
{% endif%}
|
||||
{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
{{ '}' }}
|
||||
);
|
||||
{% if cmd.result %}
|
||||
{% if 'enum_type' in cmd.result %}
|
||||
auto retval = {{ string_to_enum(cmd.result.enum_type) }}({{ var_to_cpp(cmd.result) }}(result.value()));
|
||||
{% elif 'object_type' in cmd.result %}
|
||||
json retval_json = result.value();
|
||||
{{ result_type(cmd.result) }} retval = retval_json;
|
||||
{% elif 'array_type' in cmd.result %}
|
||||
{{ result_type(cmd.result) }} retval (result.value().begin(), result.value().end());
|
||||
{% else %}
|
||||
auto retval = {{ var_to_cpp(cmd.result) }}(result.value());
|
||||
{% endif %}
|
||||
return retval;
|
||||
{% endif %}
|
||||
}
|
||||
{% if not loop.last %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor> error_state_monitor;
|
||||
|
||||
std::optional<Mapping> get_mapping() {
|
||||
return _mapping;
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Everest::error::ErrorManagerReq> error_manager;
|
||||
Everest::ModuleAdapter* const _adapter;
|
||||
Requirement _req;
|
||||
std::optional<Mapping> _mapping;
|
||||
};
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,27 @@
|
||||
{% from "helper_macros.j2" import handle_cmd_signature, print_license_header %}
|
||||
{{ print_license_header(info.license_header) }}
|
||||
|
||||
#include "{{ info.class_name}}.hpp"
|
||||
|
||||
namespace module {
|
||||
namespace {{ info.interface_implementation_id }} {
|
||||
|
||||
void {{ info.class_name }}::init() {
|
||||
|
||||
}
|
||||
|
||||
void {{ info.class_name }}::ready() {
|
||||
|
||||
}
|
||||
{% for cmd in cmds %}
|
||||
|
||||
{{ handle_cmd_signature(cmd, info.class_name, '::'+info.interface) }}{
|
||||
// your code for cmd {{ cmd.name }} goes here
|
||||
{% if cmd.result %}
|
||||
return {{ cmd.result.json_type|create_dummy_result }};
|
||||
{% endif %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
} // namespace {{ info.interface_implementation_id }}
|
||||
} // namespace module
|
||||
@@ -0,0 +1,61 @@
|
||||
{% from "helper_macros.j2" import handle_cmd_signature, print_template_info, insert_block, cpp_type, print_license_header %}
|
||||
{{ print_license_header(info.license_header) }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('3', 'marked regions will be kept') }}
|
||||
|
||||
#include <{{ info.base_class_header }}>
|
||||
|
||||
#include "{{ info.module_header }}"
|
||||
|
||||
{{ insert_block(info.blocks.add_headers) }}
|
||||
|
||||
namespace module {
|
||||
namespace {{ info.interface_implementation_id }} {
|
||||
|
||||
struct Conf {
|
||||
{% for item in info.config %}
|
||||
{{ cpp_type(item) }} {{ item.name }};
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
class {{ info.class_name }} : public {{ info.class_parent}} {
|
||||
public:
|
||||
{{ info.class_name }}() = delete;
|
||||
{{ info.class_name }}(Everest::ModuleAdapter* ev, const Everest::PtrContainer<{{ info.module_class }}> &mod, Conf& config) :
|
||||
{{ info.class_parent }}(ev, "{{ info.interface_implementation_id }}"),
|
||||
mod(mod),
|
||||
config(config)
|
||||
{};
|
||||
|
||||
{{ insert_block(info.blocks.public_defs, indent=4) }}
|
||||
|
||||
protected:
|
||||
{% if not cmds %}
|
||||
// no commands defined for this interface
|
||||
{% else %}
|
||||
// command handler functions (virtual)
|
||||
{% for cmd in cmds %}
|
||||
{{ handle_cmd_signature(cmd, none, '::'+info.interface) }} override;
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{{ insert_block(info.blocks.protected_defs, indent=4) }}
|
||||
|
||||
private:
|
||||
const Everest::PtrContainer<{{ info.module_class }}>& mod;
|
||||
const Conf& config;
|
||||
|
||||
virtual void init() override;
|
||||
virtual void ready() override;
|
||||
|
||||
{{ insert_block(info.blocks.private_defs, indent=4) }}
|
||||
};
|
||||
|
||||
{{ insert_block(info.blocks.after_class) }}
|
||||
|
||||
} // namespace {{ info.interface_implementation_id }}
|
||||
} // namespace module
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,172 @@
|
||||
{% from "helper_macros.j2" import print_template_info, var_to_cpp, print_spdx_line %}
|
||||
{{ print_spdx_line('Apache-2.0') }}
|
||||
{{ print_template_info('5') }}
|
||||
|
||||
#include "{{ info.ld_ev_header }}"
|
||||
|
||||
#ifdef EVEREST_COVERAGE_ENABLED
|
||||
#include <everest/helpers/coverage.hpp>
|
||||
#endif
|
||||
|
||||
#include "{{ info.module_header }}"
|
||||
{% for impl in provides %}
|
||||
#include "{{ impl.class_header }}"
|
||||
{% endfor %}
|
||||
|
||||
#include <framework/runtime.hpp>
|
||||
#include <utils/types.hpp>
|
||||
#include <generated/version_information.hpp>
|
||||
#ifndef PROJECT_NAME
|
||||
#define PROJECT_NAME "undefined project"
|
||||
#endif
|
||||
#ifndef PROJECT_VERSION
|
||||
#define PROJECT_VERSION "undefined version"
|
||||
#endif
|
||||
#ifndef GIT_VERSION
|
||||
#define GIT_VERSION "undefined git version"
|
||||
#endif
|
||||
|
||||
{% if info.enable_global_errors %}
|
||||
#include <utils/error/error_manager_req_global.hpp>
|
||||
|
||||
{% endif %}
|
||||
namespace module {
|
||||
|
||||
// FIXME (aw): could this way of keeping static variables be changed somehow?
|
||||
static Everest::ModuleAdapter adapter {};
|
||||
static Everest::PtrContainer<{{info.class_name }}> mod_ptr {};
|
||||
|
||||
// per module configs
|
||||
{# FIXME (aw): instead of being static, this could also be inside a map #}
|
||||
{% for impl in provides %}
|
||||
static {{ impl.id }}::Conf {{ impl.id }}_config;
|
||||
{% endfor %}
|
||||
static Conf module_conf;
|
||||
static ModuleInfo module_info;
|
||||
|
||||
{% if info.enable_global_errors %}
|
||||
void subscribe_global_all_errors(
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback
|
||||
) {
|
||||
adapter.get_global_error_manager()->subscribe_global_all_errors(callback, clear_callback);
|
||||
}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor> get_global_error_state_monitor() {
|
||||
return adapter.get_global_error_state_monitor();
|
||||
}
|
||||
|
||||
{% endif %}
|
||||
|
||||
std::shared_ptr<Everest::config::ConfigServiceClient> get_config_service_client() {
|
||||
return adapter.get_config_service_client();
|
||||
}
|
||||
|
||||
void LdEverest::init(ModuleConfigs module_configs, const ModuleInfo& mod_info) {
|
||||
EVLOG_debug << "init() called on module {{ info.name }}";
|
||||
|
||||
// populate config for provided implementations
|
||||
{% for impl in provides %}
|
||||
auto {{ impl.id }}_config_input = std::move(module_configs["{{ impl.id }}"]);
|
||||
{% for item in impl.config %}
|
||||
{{ impl.id }}_config.{{ item.name }} = std::get<{{ item.cpp_type }}>({{ impl.id }}_config_input["{{ item.name }}"]);
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
{% if info.module_config|length %}
|
||||
|
||||
{% for item in info.module_config %}
|
||||
module_conf.{{ item.name }} = std::get<{{ item.cpp_type }}>(module_configs["!module"]["{{ item.name }}"]);
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
module_info = mod_info;
|
||||
|
||||
mod_ptr->init();
|
||||
}
|
||||
|
||||
void LdEverest::ready() {
|
||||
EVLOG_debug << "ready() called on module {{ info.name }}";
|
||||
mod_ptr->ready();
|
||||
}
|
||||
|
||||
void register_module_adapter(Everest::ModuleAdapter module_adapter) {
|
||||
adapter = std::move(module_adapter);
|
||||
}
|
||||
|
||||
std::vector<Everest::cmd> everest_register(const RequirementInitialization& requirement_init) {
|
||||
EVLOG_debug << "everest_register() called on module {{ info.name }}";
|
||||
|
||||
adapter.check_complete();
|
||||
|
||||
{% for impl in provides %}
|
||||
auto p_{{ impl.id }} = std::make_unique<{{ impl.id }}::{{ impl.class_name }}>(&adapter, mod_ptr, {{ impl.id }}_config);
|
||||
adapter.gather_cmds(*p_{{ impl.id }});
|
||||
|
||||
{% endfor %}
|
||||
{% for requirement in requires %}
|
||||
{# FIXME: needs refactoring #}
|
||||
{% if requirement.is_vector %}
|
||||
auto r_{{ requirement.id }} = std::vector<std::unique_ptr<{{ requirement.class_name }}>>();
|
||||
if (auto it = requirement_init.find("{{ requirement.id }}"); it != requirement_init.end()) {
|
||||
for (const auto& requirement_initializer : (*it).second) {
|
||||
auto requirement_module_id = requirement_initializer.fulfillment.module_id;
|
||||
auto requirement = requirement_initializer.requirement;
|
||||
auto mapping = requirement_initializer.mapping;
|
||||
r_{{ requirement.id }}.emplace_back(std::make_unique<{{ requirement.class_name }}>(&adapter, requirement, requirement_module_id, mapping));
|
||||
}
|
||||
}
|
||||
{% else %}
|
||||
std::string r_{{ requirement.id }}_requirement_module_id;
|
||||
Requirement r_{{ requirement.id }}_requirement;
|
||||
std::optional<Mapping> r_{{ requirement.id }}_mapping;
|
||||
if (auto it = requirement_init.find("{{ requirement.id }}"); it != requirement_init.end()) {
|
||||
auto requirement_initializer = (*it).second;
|
||||
if (requirement_initializer.size() > 0) {
|
||||
r_{{ requirement.id }}_requirement_module_id = requirement_initializer.at(0).fulfillment.module_id;
|
||||
r_{{ requirement.id }}_requirement = requirement_initializer.at(0).requirement;
|
||||
r_{{ requirement.id }}_mapping = requirement_initializer.at(0).mapping;
|
||||
}
|
||||
}
|
||||
auto r_{{ requirement.id }} = std::make_unique<{{ requirement.class_name }}>(&adapter, r_{{ requirement.id }}_requirement, r_{{ requirement.id }}_requirement_module_id, r_{{ requirement.id }}_mapping);
|
||||
{% endif %}
|
||||
{% else %}
|
||||
(void) requirement_init; // no requirements -> unused requirement initialization
|
||||
{% endfor %}
|
||||
|
||||
{% if info.enable_external_mqtt %}
|
||||
static Everest::MqttProvider mqtt_provider(adapter);
|
||||
{% endif %}
|
||||
static Everest::TelemetryProvider telemetry_provider(adapter);
|
||||
|
||||
static {{ info.class_name }} module(
|
||||
module_info,
|
||||
{%- if info.enable_external_mqtt %}mqtt_provider, {% endif -%}
|
||||
{%- if info.enable_telemetry %}telemetry_provider, {% endif -%}
|
||||
{%- for impl in provides -%}
|
||||
std::move(p_{{ impl.id }}){{ ', ' }}
|
||||
{%- endfor -%}
|
||||
{%- for requirement in requires -%}
|
||||
std::move(r_{{ requirement.id }}){{ ', ' }}
|
||||
{%- endfor -%}
|
||||
module_conf);
|
||||
|
||||
mod_ptr.set(&module);
|
||||
|
||||
return adapter.registered_commands;
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef EVEREST_COVERAGE_ENABLED
|
||||
everest::helpers::install_signal_handlers_for_gcov();
|
||||
#endif
|
||||
|
||||
auto module_loader = Everest::ModuleLoader(argc, argv, Everest::ModuleCallbacks(
|
||||
module::register_module_adapter, module::everest_register,
|
||||
module::LdEverest::init, module::LdEverest::ready),
|
||||
{PROJECT_NAME, PROJECT_VERSION, GIT_VERSION});
|
||||
|
||||
return module_loader.initialize();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{% from "helper_macros.j2" import print_template_info, print_spdx_line %}
|
||||
{{ print_spdx_line('Apache-2.0') }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('3') }}
|
||||
|
||||
#include <framework/ModuleAdapter.hpp>
|
||||
#include <framework/everest.hpp>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
namespace module {
|
||||
|
||||
// helper class for invoking private functions on module
|
||||
struct LdEverest {
|
||||
static void init(ModuleConfigs module_configs, const ModuleInfo& info);
|
||||
static void ready();
|
||||
};
|
||||
|
||||
void register_module_adapter(Everest::ModuleAdapter module_adapter);
|
||||
|
||||
std::vector<Everest::cmd> everest_register(const RequirementInitialization& requirement_init);
|
||||
|
||||
{% if info.enable_global_errors %}
|
||||
void subscribe_global_all_errors(
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback
|
||||
);
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor> get_global_error_state_monitor();
|
||||
{% endif %}
|
||||
|
||||
std::shared_ptr<Everest::config::ConfigServiceClient> get_config_service_client();
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% from "helper_macros.j2" import print_license_header %}
|
||||
{{ print_license_header(info.license_header) }}
|
||||
#include "{{ info.module_header }}"
|
||||
|
||||
namespace module {
|
||||
|
||||
void {{ info.class_name }}::init() {
|
||||
{% for impl in provides %}
|
||||
invoke_init(*p_{{ impl.id }});
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
void {{ info.class_name }}::ready() {
|
||||
{% for impl in provides %}
|
||||
invoke_ready(*p_{{ impl.id }});
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
} // namespace module
|
||||
@@ -0,0 +1,109 @@
|
||||
{% from "helper_macros.j2" import print_template_info, insert_block, cpp_type, print_license_header %}
|
||||
{{ print_license_header(info.license_header) }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('2', 'marked regions will be kept') }}
|
||||
|
||||
#include "{{ info.ld_ev_header }}"
|
||||
|
||||
{% for impl in provides %}
|
||||
{% if loop.first %}
|
||||
// headers for provided interface implementations
|
||||
{% endif %}
|
||||
#include <{{ impl.base_class_header }}>
|
||||
{% endfor %}
|
||||
|
||||
{% for interface in requires %}
|
||||
{% if loop.first %}
|
||||
// headers for required interface implementations
|
||||
{% endif %}
|
||||
#include <{{ interface.exports_header }}>
|
||||
{% endfor %}
|
||||
|
||||
{{ insert_block(info.blocks.add_headers) }}
|
||||
|
||||
namespace module {
|
||||
|
||||
struct Conf {
|
||||
{% for item in info.module_config %}
|
||||
{{ cpp_type(item) }} {{ item.name }};
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
class {{ info.class_name }} : public Everest::ModuleBase {
|
||||
public:
|
||||
{{ info.class_name }}() = delete;
|
||||
{{ info.class_name }}(
|
||||
const ModuleInfo& info,
|
||||
{% if info.enable_external_mqtt %}
|
||||
Everest::MqttProvider& mqtt_provider,
|
||||
{% endif %}
|
||||
{% if info.enable_telemetry %}
|
||||
Everest::TelemetryProvider& telemetry,
|
||||
{% endif %}
|
||||
{% for impl in provides %}
|
||||
std::unique_ptr<{{ impl.base_class }}> p_{{ impl.id }},
|
||||
{% endfor %}
|
||||
{% for requirement in requires %}
|
||||
{% if requirement.is_vector %}
|
||||
std::vector<std::unique_ptr<{{ requirement.class_name }}>> r_{{ requirement.id }},
|
||||
{% else %}
|
||||
std::unique_ptr<{{ requirement.class_name }}> r_{{ requirement.id }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Conf& config
|
||||
) :
|
||||
ModuleBase(info),
|
||||
{% if info.enable_external_mqtt %}
|
||||
mqtt(mqtt_provider),
|
||||
{% endif %}
|
||||
{% if info.enable_telemetry %}
|
||||
telemetry(telemetry),
|
||||
{% endif %}
|
||||
{% for impl in provides %}
|
||||
p_{{ impl.id }}(std::move(p_{{ impl.id }})),
|
||||
{% endfor %}
|
||||
{% for requirement in requires %}
|
||||
r_{{ requirement.id }}(std::move(r_{{ requirement.id }})),
|
||||
{% endfor %}
|
||||
config(config)
|
||||
{};
|
||||
|
||||
{% if info.enable_external_mqtt %}
|
||||
Everest::MqttProvider& mqtt;
|
||||
{% endif %}
|
||||
{% if info.enable_telemetry %}
|
||||
Everest::TelemetryProvider& telemetry;
|
||||
{% endif %}
|
||||
{% for impl in provides %}
|
||||
const std::unique_ptr<{{ impl.base_class }}> p_{{ impl.id }};
|
||||
{% endfor %}
|
||||
{% for requirement in requires %}
|
||||
{% if requirement.is_vector %}
|
||||
const std::vector<std::unique_ptr<{{ requirement.class_name }}>> r_{{ requirement.id }};
|
||||
{% else %}
|
||||
const std::unique_ptr<{{ requirement.class_name }}> r_{{ requirement.id }};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
const Conf& config;
|
||||
|
||||
{{ insert_block(info.blocks.public_defs, indent=4) }}
|
||||
|
||||
protected:
|
||||
{{ insert_block(info.blocks.protected_defs, indent=4) }}
|
||||
|
||||
private:
|
||||
friend class LdEverest;
|
||||
void init();
|
||||
void ready();
|
||||
|
||||
{{ insert_block(info.blocks.private_defs, indent=4) }}
|
||||
|
||||
};
|
||||
|
||||
{{ insert_block(info.blocks.after_class) }}
|
||||
|
||||
} // namespace module
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,243 @@
|
||||
{% from "helper_macros.j2" import print_template_info, print_spdx_line, string_to_enum, enum_to_string, enum_to_string_view %}
|
||||
{{ print_spdx_line('Apache-2.0') }}
|
||||
#ifndef {{ info.hpp_guard }}
|
||||
#define {{ info.hpp_guard }}
|
||||
|
||||
{{ print_template_info('5') }}
|
||||
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
{% if 'type_headers' in info %}
|
||||
{% for type_header in info.type_headers %}
|
||||
#include <{{type_header}}>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
using nlohmann::json;
|
||||
|
||||
{% if not enums %}
|
||||
// no enums defined for this interface
|
||||
{% else %}
|
||||
|
||||
// enums of {{ info.interface_name }}
|
||||
|
||||
{% for enum in enums %}
|
||||
{% for namespace in info.namespace %}
|
||||
namespace {{ namespace }} {
|
||||
{% endfor %}
|
||||
enum class {{ enum.enum_type}}
|
||||
{
|
||||
{% for e in enum.enum %}
|
||||
{{e}},
|
||||
{% endfor %}
|
||||
};
|
||||
|
||||
/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
|
||||
/// \returns a string representation of the {{ enum.enum_type }}
|
||||
inline std::string {{ enum.enum_type | snake_case }}_to_string(const {{ enum.enum_type }}& e) {
|
||||
switch (e) {
|
||||
{% for e in enum.enum %}
|
||||
case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
|
||||
}
|
||||
|
||||
/// \brief Converts the given {{ enum.enum_type }} \p e to human readable string
|
||||
/// \returns a string_view representation of the {{ enum.enum_type }}
|
||||
inline constexpr std::string_view {{ enum.enum_type | snake_case }}_to_string_view(const {{ enum.enum_type }}& e) {
|
||||
switch (e) {
|
||||
{% for e in enum.enum %}
|
||||
case {{ enum.enum_type }}::{{ e }}: return "{{e}}";
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
throw std::out_of_range("No known string conversion for provided enum of type {{ enum.enum_type }}");
|
||||
}
|
||||
|
||||
/// \brief Converts the given std::string \p s to {{ enum.enum_type }}
|
||||
/// \returns a {{ enum.enum_type }} from a string representation
|
||||
inline {{ enum.enum_type }} string_to_{{ enum.enum_type | snake_case }}(const std::string& s) {
|
||||
{% for e in enum.enum %}
|
||||
if (s == "{{e}}") {
|
||||
return {{ enum.enum_type }}::{{ e }};
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
throw std::out_of_range("Provided string " + s + " could not be converted to enum of type {{ enum.enum_type }}");
|
||||
}
|
||||
|
||||
/// \brief Writes the string representation of the given {{ enum.enum_type }} \p {{ enum.enum_type | snake_case }} to the given output stream \p os
|
||||
/// \returns an output stream with the {{ enum.enum_type }} written to
|
||||
inline std::ostream& operator<<(std::ostream& os, const types::{{ info.interface_name }}::{{ enum.enum_type }}& {{ enum.enum_type | snake_case }}) {
|
||||
os << types::{{info.interface_name}}::{{ enum.enum_type | snake_case }}_to_string({{ enum.enum_type | snake_case }});
|
||||
return os;
|
||||
}
|
||||
|
||||
{% for namespace in info.namespace|reverse %}
|
||||
} // namespace {{namespace}}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
{% endif%}
|
||||
|
||||
{% if not types %}
|
||||
// no types defined for this interface
|
||||
{% else %}
|
||||
// types of {{ info.interface_name }}
|
||||
{% for namespace in info.namespace %}
|
||||
namespace {{ namespace }} {
|
||||
{% endfor %}
|
||||
|
||||
{% for parsed_type in types %}
|
||||
{% if parsed_type.properties|length > 0 %}
|
||||
struct {{ parsed_type.name }} {
|
||||
{% for property in parsed_type.properties %}
|
||||
{# Constraints will be checked by the framework #}
|
||||
{{ 'std::optional<' if not property.required -}}
|
||||
{{ property.type -}}
|
||||
{{ '>' if not property.required -}}
|
||||
{{ ' ' + property.name + ';' }} ///< {{ property.info.description }}
|
||||
{% endfor %}
|
||||
|
||||
/// \brief Conversion from a given {{ parsed_type.name }} \p k to a given json object \p j
|
||||
friend void to_json(json& j, const {{ parsed_type.name }}& k) {
|
||||
// the required parts of the type
|
||||
{% if parsed_type.properties|selectattr('required')|list|length %}
|
||||
j = json{
|
||||
{%- endif %}
|
||||
{%- for property in parsed_type.properties %}
|
||||
{%- if property.required +%}
|
||||
{"{{property.name}}",
|
||||
{%- if property.enum %} {{ enum_to_string_view(property.type) }}(k.{{ property.name }})
|
||||
{%- else %}
|
||||
{%- if property.type == 'DateTime' %} k.{{property.name}}.to_rfc3339()
|
||||
{%- else %} k.{{property.name}}
|
||||
{%- endif %}
|
||||
{%- endif %}},
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% if not parsed_type.properties|selectattr('required')|list|length %}
|
||||
j = json ({});
|
||||
{%- else +%}
|
||||
};
|
||||
{%- endif %}
|
||||
|
||||
// the optional parts of the type
|
||||
{% for property in parsed_type.properties %}
|
||||
{% if not property.required %}
|
||||
if (k.{{property.name}}) {
|
||||
{% if property.type.startswith('std::vector<') %}
|
||||
{%- if parsed_type.properties|selectattr('required')|list|length %}
|
||||
j.emplace("{{property.name}}", *k.{{property.name}});
|
||||
{%- else %}
|
||||
{#only optional keys in json#}
|
||||
{#TODO: add key to json when there are no required keys but multiple optional keys#}
|
||||
{# FIXME: this is never generated?#}
|
||||
if (j.size() == 0) {
|
||||
j = json{{'{{"'+property.name+'", json::array()}};'}}
|
||||
} else {
|
||||
j.emplace("{{property.name}}", *k.{{property.name}});
|
||||
}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{%- if property.enum %}
|
||||
j.emplace("{{property.name}}", {{ enum_to_string_view(property.type) }}(*k.{{ property.name }}));
|
||||
{%- else %}
|
||||
{%- if property.type == 'DateTime' %}
|
||||
j.emplace("{{property.name}}", k.{{property.name}}->to_rfc3339());
|
||||
{%- else %}
|
||||
j.emplace("{{property.name}}", *k.{{property.name}});
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{% endif %}
|
||||
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
/// \brief Conversion from a given json object \p j to a given {{ parsed_type.name }} \p k
|
||||
friend void from_json(const json& j, {{ parsed_type.name }}& k) {
|
||||
// the required parts of the type
|
||||
{% for property in parsed_type.properties %}
|
||||
{% if property.required %}
|
||||
{% if property.type.startswith('std::vector<') %}
|
||||
k.{{property.name}} = std::move(j.at("{{property.name}}").get<{{property.type}}>());
|
||||
{% else %}
|
||||
k.{{property.name}} =
|
||||
{%- if property.enum %} {{ string_to_enum(property.type) }}(j.at("{{property.name}}"))
|
||||
{%- else %}
|
||||
{%- if property.type == 'DateTime' %} DateTime(std::string(j.at("{{property.name}}")));
|
||||
{%- else %} j.at("{{property.name}}")
|
||||
{%- endif %}
|
||||
{%- endif %};
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{%- endfor %}
|
||||
|
||||
// the optional parts of the type
|
||||
auto it = j.end();
|
||||
{% for property in parsed_type.properties %}
|
||||
{% if not property.required %}
|
||||
it = it = j.find("{{property.name}}");
|
||||
if (it != j.end()) {
|
||||
{% if property.type.startswith('std::vector<') %}
|
||||
k.{{property.name}} = j.at("{{property.name}}").get<{{property.type}}>();
|
||||
{% else %}
|
||||
{%- if property.enum %}
|
||||
k.{{property.name}} = {{ string_to_enum(property.type) }}(it->get<std::string>());
|
||||
{%- else %}
|
||||
k.{{property.name}} = it->get<{{property.type}}>();
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
/// \brief Compares objects of type {{ parsed_type.name }} for equality
|
||||
friend constexpr bool operator==(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
|
||||
{%- for var, tuple in [('k.', 'lhs'), ('l.', 'rhs')] +%}
|
||||
const auto& {{ tuple }}_tuple = std::tie(
|
||||
{%- for property in parsed_type.properties +%}
|
||||
{{ var }}{{ property.name }}
|
||||
{%- if not loop.last %},
|
||||
{%- endif %}
|
||||
{%- endfor +%}
|
||||
);
|
||||
{%- endfor +%}
|
||||
return lhs_tuple == rhs_tuple;
|
||||
}
|
||||
|
||||
/// \brief Compares objects of type {{ parsed_type.name }} for inequality
|
||||
friend constexpr bool operator!=(const {{ parsed_type.name }}& k, const {{ parsed_type.name }}& l) {
|
||||
return not operator==(k, l);
|
||||
}
|
||||
|
||||
/// \brief Writes the string representation of the given {{ parsed_type.name }} \p k to the given output stream \p os
|
||||
/// \returns an output stream with the {{ parsed_type.name }} written to
|
||||
friend std::ostream& operator<<(std::ostream& os, const {{ parsed_type.name }}& k) {
|
||||
os << json(k).dump(4);
|
||||
return os;
|
||||
}
|
||||
|
||||
};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for namespace in info.namespace|reverse %}
|
||||
} // namespace {{namespace}}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
#endif // {{ info.hpp_guard }}
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
Provide type parsing functionality.
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
"""
|
||||
|
||||
from . import helpers
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
import stringcase
|
||||
|
||||
|
||||
class TypeParser:
|
||||
"""Provide generation of type headers from type definitions."""
|
||||
validators = None
|
||||
templates = None
|
||||
all_types = {}
|
||||
validated_type_defs = {}
|
||||
|
||||
@classmethod
|
||||
def parse_type_url(cls, type_url: str) -> Dict:
|
||||
"""Parse a global type URL in the following format /filename#/typename."""
|
||||
|
||||
type_dict = {
|
||||
'type_relative_path': None,
|
||||
'namespaced_type': None,
|
||||
'header_file': None,
|
||||
'type_name': None
|
||||
}
|
||||
if not type_url.startswith('/'):
|
||||
raise Exception('type_url: ' + type_url + ' needs to start with a "/".')
|
||||
if '#/' not in type_url:
|
||||
raise Exception('type_url: ' + type_url + ' needs to refer to a specific type with "#/TypeName".')
|
||||
type_relative_path, prop_type = type_url.split('#/')
|
||||
type_relative_path = Path(type_relative_path[1:])
|
||||
|
||||
namespaced_type = 'types::' + '::'.join(type_relative_path.parts) + f'::{prop_type}'
|
||||
type_dict['type_relative_path'] = type_relative_path
|
||||
type_dict['namespaced_type'] = namespaced_type
|
||||
type_dict['type_name'] = prop_type
|
||||
|
||||
return type_dict
|
||||
|
||||
@classmethod
|
||||
def does_type_exist(cls, type_url: str, json_type: str):
|
||||
"""Checks if the referenced type exists"""
|
||||
if type_url not in TypeParser.all_types:
|
||||
TypeParser.all_types[type_url] = TypeParser.parse_type_url(type_url=type_url)
|
||||
type_dict = TypeParser.all_types[type_url]
|
||||
type_path = helpers.resolve_everest_dir_path('types' / type_dict['type_relative_path'].with_suffix('.yaml'))
|
||||
if not type_path or not type_path.exists():
|
||||
raise helpers.EVerestParsingException(
|
||||
'$ref: ' + type_url + f' referenced type file "{type_path}" does not exist.')
|
||||
if type_path not in TypeParser.validated_type_defs:
|
||||
TypeParser.validated_type_defs[type_path] = helpers.load_validated_type_def(
|
||||
type_path, TypeParser.validators['type'])
|
||||
|
||||
if type_dict['type_name'] not in TypeParser.validated_type_defs[type_path]['types']:
|
||||
raise helpers.EVerestParsingException('$ref: ' + type_url + ' referenced type "' +
|
||||
type_dict['type_name'] + f'" does not exist in type file "{type_path}".')
|
||||
|
||||
type_schema = TypeParser.validated_type_defs[type_path]['types'][type_dict['type_name']]
|
||||
|
||||
if json_type != type_schema['type']:
|
||||
raise helpers.EVerestParsingException('$ref: ' + type_url + ' referenced type "' +
|
||||
type_dict['type_name'] + f'" in type file "{type_path}"' +
|
||||
f' should be of type "{json_type}" but is of type: "' +
|
||||
type_schema['type'] + '".')
|
||||
|
||||
@classmethod
|
||||
def generate_tmpl_data_for_type(cls, type_with_namespace, type_def):
|
||||
"""Generate template data based on the provided type and type definition."""
|
||||
helpers.parsed_enums.clear()
|
||||
helpers.parsed_types.clear()
|
||||
helpers.type_headers.clear()
|
||||
types = []
|
||||
enums = []
|
||||
|
||||
for type_name, type_properties in type_def.get('types', {}).items():
|
||||
type_url = f'/{type_with_namespace["relative_path"]}#/{type_name}'
|
||||
TypeParser.all_types[type_url] = TypeParser.parse_type_url(type_url=type_url)
|
||||
try:
|
||||
(_type_info, enum_info) = helpers.extended_build_type_info(type_name, type_properties, type_file=True)
|
||||
if enum_info:
|
||||
enums.append(enum_info)
|
||||
except helpers.EVerestParsingException as e:
|
||||
raise helpers.EVerestParsingException(f'Error parsing type {type_name}: {e}')
|
||||
|
||||
for parsed_enum in helpers.parsed_enums:
|
||||
enum_info = {
|
||||
'name': parsed_enum['name'],
|
||||
'description': parsed_enum['description'],
|
||||
'enum_type': stringcase.capitalcase(parsed_enum['name']),
|
||||
'enum': parsed_enum['enums']
|
||||
}
|
||||
enums.append(enum_info)
|
||||
|
||||
for parsed_type in helpers.parsed_types:
|
||||
parsed_type['name'] = stringcase.capitalcase(parsed_type['name'])
|
||||
types.append(parsed_type)
|
||||
|
||||
type_headers = sorted(helpers.type_headers)
|
||||
|
||||
# Remove the header itself from the includes.
|
||||
own_header = helpers.generate_header_for_type(type_with_namespace["relative_path"])
|
||||
type_headers = [header for header in type_headers if str(header) != str(own_header)]
|
||||
|
||||
# sort types, so no forward declaration is necessary
|
||||
sorted_types: List = []
|
||||
for struct_type in types:
|
||||
insert_at: int = 0
|
||||
for dep_struct_type in struct_type['depends_on']:
|
||||
|
||||
for i, _entry in enumerate(sorted_types):
|
||||
# the new one depends on the current
|
||||
if sorted_types[i]['name'] == dep_struct_type:
|
||||
insert_at = max(insert_at, i + 1)
|
||||
break
|
||||
|
||||
sorted_types.insert(insert_at, struct_type)
|
||||
|
||||
tmpl_data = {
|
||||
'info': {
|
||||
'type': type_with_namespace['namespace'],
|
||||
'desc': type_def['description'],
|
||||
'type_headers': type_headers,
|
||||
},
|
||||
'enums': enums,
|
||||
'types': sorted_types,
|
||||
}
|
||||
|
||||
return tmpl_data
|
||||
|
||||
@classmethod
|
||||
def load_type_definition(cls, type_path: Path):
|
||||
"""Load a type definition from the provided path and check its last modification time."""
|
||||
if type_path not in TypeParser.validated_type_defs:
|
||||
TypeParser.validated_type_defs[type_path] = helpers.load_validated_type_def(
|
||||
type_path, TypeParser.validators['type'])
|
||||
|
||||
last_mtime = type_path.stat().st_mtime
|
||||
|
||||
return TypeParser.validated_type_defs[type_path], last_mtime
|
||||
|
||||
@classmethod
|
||||
def generate_type_info(cls, type_with_namespace, all_types) -> Tuple:
|
||||
"""Generate type template data."""
|
||||
try:
|
||||
type_def, last_mtime = TypeParser.load_type_definition(type_with_namespace['path'])
|
||||
except Exception as e:
|
||||
if not all_types:
|
||||
raise
|
||||
else:
|
||||
print(f'Ignoring type {type_with_namespace["namespace"]} with reason: {e}')
|
||||
return
|
||||
|
||||
tmpl_data = TypeParser.generate_tmpl_data_for_type(type_with_namespace, type_def)
|
||||
|
||||
return (tmpl_data, last_mtime)
|
||||
|
||||
@classmethod
|
||||
def generate_type_headers(cls, type_with_namespace, all_types, output_dir):
|
||||
"""Render template data to generate type headers."""
|
||||
tmpl_data, last_mtime = TypeParser.generate_type_info(type_with_namespace, all_types)
|
||||
|
||||
types_parts = {'types': None}
|
||||
|
||||
output_path = output_dir / type_with_namespace['relative_path']
|
||||
types_file = output_path.with_suffix('.hpp')
|
||||
output_path = output_path.parent
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
namespaces = ['types']
|
||||
namespaces.extend(type_with_namespace['relative_path'].parts)
|
||||
|
||||
tmpl_data['info']['interface_name'] = f'{type_with_namespace["namespace"]}'
|
||||
tmpl_data['info']['namespace'] = namespaces
|
||||
tmpl_data['info']['hpp_guard'] = 'TYPES_' + helpers.snake_case(
|
||||
''.join(type_with_namespace['uppercase_path'])).upper() + '_TYPES_HPP'
|
||||
|
||||
types_parts['types'] = {
|
||||
'path': types_file,
|
||||
'content': TypeParser.templates['types.hpp'].render(tmpl_data),
|
||||
'last_mtime': last_mtime,
|
||||
'template_path': Path(TypeParser.templates['types.hpp'].filename),
|
||||
'printable_name': types_file.relative_to(output_path.parent)
|
||||
}
|
||||
|
||||
return types_parts
|
||||
5
tools/EVerest-main/applications/utils/everest-testing/.gitignore
vendored
Normal file
5
tools/EVerest-main/applications/utils/everest-testing/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
build
|
||||
__pycache__
|
||||
*.egg-info
|
||||
.pytest_cache
|
||||
results.xml
|
||||
@@ -0,0 +1,26 @@
|
||||
load("//applications/utils:requirements.bzl", "requirement")
|
||||
load("@rules_python//python:defs.bzl", "py_library")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
exports_files(
|
||||
["BUILD.bazel"],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "everest-testing",
|
||||
srcs = glob(["src/**/*.py"]),
|
||||
imports = ["src"],
|
||||
visibility = ["//visibility:public"],
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
deps = [
|
||||
requirement("pytest"),
|
||||
requirement("pytest-asyncio"),
|
||||
requirement("python-dateutil"),
|
||||
requirement("paho-mqtt"),
|
||||
requirement("pyftpdlib"),
|
||||
requirement("ocpp"),
|
||||
requirement("websockets"),
|
||||
requirement("pyOpenSSL"),
|
||||
requirement("pyyaml"),
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
ev_create_pip_install_targets(
|
||||
PACKAGE_NAME
|
||||
"everest-testing"
|
||||
)
|
||||
|
||||
ev_create_python_wheel_targets(
|
||||
PACKAGE_NAME
|
||||
"everest-testing"
|
||||
)
|
||||
148
tools/EVerest-main/applications/utils/everest-testing/README.md
Normal file
148
tools/EVerest-main/applications/utils/everest-testing/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Everest Testing
|
||||
|
||||
This python package provides utility for testing EVerest with pytest.
|
||||
|
||||
The utilities are seperated into
|
||||
|
||||
- core_utils: Providing classes and fixtures to get a running and properly configured EVerest instance
|
||||
- ocpp_utils (under development): Providing class and fixtures to test against an OCPP1.6J or OCPP2.0.1J central system
|
||||
|
||||
## Core Utils
|
||||
|
||||
### everest_core
|
||||
The EverestCore class wraps the running EVerest instance and takes care of providing a proper set up environment (including
|
||||
the generation of temporary directories and adjusting the configuration accordingly. Note that in order to do so, EverestCore generates
|
||||
a temporary configuration file.)
|
||||
|
||||
### test_controller
|
||||
|
||||
Controller that can be used to start/stop the Everest instance and send events to control/simulate the stack's behavior.
|
||||
|
||||
### Fixtures
|
||||
|
||||
The core_utils basically provide two fixtures that you can require in your test cases:
|
||||
|
||||
- **everest_core** The main fixture `everest_core` can be used to start and stop the EVerest application.
|
||||
- **test_controller**: Fixture that references the test_controller that can be used for control events for the test cases. This includes control over simulations that trigger events like an EV plug in, EV plug out, swipe RFID and more.
|
||||
|
||||
#### Configuration Fixtures:
|
||||
|
||||
- **core_config** Core configuration, which is the EVerest path and the configuration path (utilizes the `everest_core_config` marker.)
|
||||
- **probe_module_config** Used to provide the probe module configuration. In particular can be overriden if the probe module should require module connections.
|
||||
- **ocpp_config** Used to provide the configuration, i.e. the JSON device model, to set up the OCPP (1.6 or 2.0.1) module.
|
||||
- **evse_security_config** Used to provide the configuration to set up the EvseSecurity module.
|
||||
- **persistent_storage_config** Used to provide the configuration to set up the PersistentStorage module.
|
||||
- **everest_config_strategies**: Provides a list of additional `EverestConfigAdjustmentStrategy` instances that are called to manipulate the resulting Everest configuration.
|
||||
|
||||
### pytest markers
|
||||
|
||||
Some OCPP fixtures will parse pytest markers of test cases. The following markers can be used:
|
||||
- **everest_core_config**: Can be used to specify the everest configuration file to be used in this test case
|
||||
- **standalone_module**: Define one or several modules as standalone (multiple modules via `@pytest.mark.standalone_module("mod1","mod2")`)
|
||||
- **probe_module**: If set, the ProbeModule will be injected into the config (used by the `probe_module_config` fixture). This marker accepts optional keyword arguments `connections` and `module_id` to configure the probe module.
|
||||
- **source_certs_dir**: If set and the default `evse_security_config` fixture is used, this will cause the `EvseSecurity` module configuration to use a temporary certificates folder into which the source certificate folder trees are copied.
|
||||
- **use_temporary_persistent_store**: If set and the default `persistent_storage_config` fixture is used, this will cause the `PersistentStore` module configuration to use a temporary database.
|
||||
- **everest_config_adaptions**: Can be given instances of `EverestConfigAdjustmentStrategy` as positional arguments which will be applied to the resulting Everest configuration.
|
||||
|
||||
## OCPP utils
|
||||
|
||||
The ocpp utils provide fixture which you can require in your test cases in order to start a central system and initiate operations.
|
||||
These utilities are still under development.
|
||||
|
||||
- **central_system_v16**: Fixture that starts up an OCPP1.6 central system. Can be started as TLS or plain websocket depending on the request parameter.
|
||||
- **central_system_v201**: Fixture that starts up an OCPP2.0.1 central system. Can be started as TLS or plain websocket depending on the request parameter.
|
||||
- **charge_point_v16**: Fixture starts up an OCPP1.6 central system and provides access to the connection of the charge point that connects to it. This reference can be used to send OCPP messages initiated by the central system and to receive and validate messages from the charge point. It requires the fixtures central_system_v16 and test_controller and starts the test_controller immediately.
|
||||
- **charge_point_v201**: Fixture starts up an OCPP2.0.1 central system and provides access to the connection of the charge point that connects to it. This reference can be used to send OCPP messages initiated by the central system and to receive and validate messages from the charge point. It requires the fixtures central_system_v16 and test_controller and starts the test_controller immediately.
|
||||
- **test_utility**: Utility fixture that contains the OCPP message history, the validation mode (STRICT, EASY) and it can keep track of forbidden OCPP messages (Actions) (ones that cause a test case to fail if they are received)
|
||||
- **ftp_server**: This fixture creates a temporary directory and starts a local ftp server connected to that directory. The temporary directory is deleted after the test. It is used for Diagnostics and Logfiles
|
||||
|
||||
#### Configuration Fixtures:
|
||||
- **test_config**: This fixture is of type OcppTestConfiguration and it specifies some data that are required or can be configured for testing OCPP. If you don't override this fixture, it initiializes to some default information that is required to set up other fixtures (e.g. ChargePointId, CSMS Port). You can implement this fixture yourself in order to be able to include this information in your test cases.
|
||||
- **ocpp_config** _Overrides_ the core_util's `ocpp_config` fixture. Requires the `test_config` fixture to extract required OCPP configuration.
|
||||
|
||||
An important function that you will frequently use when writing test cases is the **wait_for_and_validate** function inside [charge_point_utils.py](src/everest/testing/ocpp_utils/charge_point_utils.py). This method waits for an expected message specified by the message_type, the action and the payload to be received. It also considers the test case meta_data that contains the message history, the validation mode and forbidden actions.
|
||||
|
||||
|
||||
### pytest markers
|
||||
|
||||
- **ocpp_version**: Can be "ocpp1.6" or "ocpp2.0.1" and is used to setup EVerest and the central system for the specific OCPP version
|
||||
- **ocpp_config**: Specification of the .json OCPP config file. Used in `ocpp_config` fixture and used as template configuration (if not specified, the OCPP config as specified in the EVerest configuration is used)
|
||||
- **ocpp_config_adaptions**: Specification of the .json OCPP config file. Used in `ocpp_config` fixture and used as template configuration (if not specified, the OCPP config as specified in the EVerest configuration is used)
|
||||
- **inject_csms_mock**: (currently only OCPP 2.0.1) If set, the `central_system_v201` will wrap any csms handler method into an unittest mock. In particular, this allows changing the CSMS behavior even after the chargepoint is started by setting side effects of the mock. See `everest.testing.ocpp_utils.charge_point_v201.inject_csms_v201_mock` docstring for an example.
|
||||
- **csms_tls**: Enable/disable TLS for the CSMS websocket server. If given without arguments, enables TLS. First argument can be `False` to explicitly disable TLS. Further optional keyword arguments `certificate`, `private_key`,`passphrase`, `root_ca` , and `verify_client_certificate` allow to overwrite SSL context options.
|
||||
- **ocpp_config_adaptions**: Can be given instances of `OCPPConfigAdjustmentStrategy` as positional arguments which will be applied to the resulting OCPP configuration.
|
||||
- **custom_central_system**: Can be given a instance of `CentralSystem` as the first positional argument which will use that instance as a central system for all fixtures that require a `central_system`.
|
||||
|
||||
|
||||
## Add a conftest.py
|
||||
|
||||
The test_controller fixture and inherently also the charge_point_v16 and charge_point_v201 require information about the directory of the EVerest application and libocpp. Those can be specified within a conftest.py. Within the conftest.py you could also override the test_config fixture for your specific setup.
|
||||
|
||||
## Set markers and override fixture to configure instances
|
||||
|
||||
The `everest_core` fixture utilizes the several configuration fixtures to configure the running instances. In order to adjust
|
||||
the configuration you can
|
||||
- set respective pytest markers to adjust the configuration for a single test / test class
|
||||
- override the specific fixtures to adjust the configuration for a whole test suite
|
||||
|
||||
Note: When overriding a fixture, be careful which pytest markers might be ignored as used by the overridden fixture!
|
||||
|
||||
### Example
|
||||
|
||||
```python
|
||||
|
||||
from everest.testing.core_utils.fixtures import *
|
||||
from everest.testing.ocpp_utils.fixtures import ocpp_config
|
||||
|
||||
@pytest.fixture
|
||||
def test_config(request) -> OcppTestConfiguration:
|
||||
# some code generating a customized OCPP test config
|
||||
...
|
||||
return custom_test_config
|
||||
|
||||
@pytest.fixture
|
||||
def core_config(request) -> EverestEnvironmentCoreConfiguration:
|
||||
# some code generating a customized EveresetCore test config
|
||||
# e.g. useful to point to local Everest configuration files
|
||||
...
|
||||
return custom_core_config
|
||||
|
||||
|
||||
@pytest.mark.probe_module
|
||||
class TestMyEverestModule:
|
||||
# ... pytest test suite that uses for all test the probe module
|
||||
|
||||
def test_a(self, test_controller):
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
_Note_: The "*" import from `core_utils.fixtures` may ensure backwards compatibility to automatically load new default fixtures in the future!
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
In order to use the provided fixtures within your test cases, a successful build of EVerest is required. Refer to https://github.com/EVerest/EVerest for this.
|
||||
|
||||
An MQTT broker needs to run on your system in order to start the test cases including EVerest. Docker can be used for this. Refer to https://everest.github.io/nightly/tutorials/docker_setup.html in order to set this up.
|
||||
|
||||
Install this package using
|
||||
|
||||
```bash
|
||||
python3 -m pip install .
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Have a look at [example_tests.py](examples/tests.py). In this file you can find and run one OCPP1.6 and one OCPP2.0.1 test case. These test cases will help you to get familiar with the fixtures provided in this package. You need a successful build of [EVerest](https://github.com/EVerest/EVerest) on your development machine in order to run the tests.
|
||||
|
||||
You can run these tests using
|
||||
|
||||
```bash
|
||||
cd examples
|
||||
python3 -m pytest tests.py --everest-prefix <path-to-EVerest>/build/dist/ --libocpp <path-to-libocpp> --log-cli-level=DEBUG
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
This file was generate using [venv](https://docs.python.org/3/library/venv.html) and [pip-licenses](https://pypi.org/project/pip-licenses/).
|
||||
|
||||
```bash
|
||||
python3 -m venv everest-testing-venv
|
||||
source everest-testing-venv/bin/activate
|
||||
```
|
||||
|
||||
Change into everest-utils/everest-testing and execute
|
||||
|
||||
```bash
|
||||
python3 -m pip install .
|
||||
python3 -m pip install pip-licenses
|
||||
pip-licenses --format=markdown --order=license --ignore-packages everest-testing
|
||||
```
|
||||
|
||||
| Name | Version | License |
|
||||
|-----------------|---------|-----------------------------------------------------------------|
|
||||
| pyOpenSSL | 23.0.0 | Apache Software License |
|
||||
| pytest-asyncio | 0.21.0 | Apache Software License |
|
||||
| cryptography | 39.0.2 | Apache Software License; BSD License |
|
||||
| packaging | 23.0 | Apache Software License; BSD License |
|
||||
| python-dateutil | 2.8.2 | Apache Software License; BSD License |
|
||||
| pycparser | 2.21 | BSD License |
|
||||
| websockets | 10.4 | BSD License |
|
||||
| paho-mqtt | 1.6.1 | Eclipse Public License v2.0 / Eclipse Distribution License v1.0 |
|
||||
| attrs | 22.2.0 | MIT License |
|
||||
| cffi | 1.15.1 | MIT License |
|
||||
| exceptiongroup | 1.1.1 | MIT License |
|
||||
| iniconfig | 2.0.0 | MIT License |
|
||||
| jsonschema | 4.17.3 | MIT License |
|
||||
| ocpp | 0.17.0 | MIT License |
|
||||
| pluggy | 1.0.0 | MIT License |
|
||||
| pyftpdlib | 1.5.7 | MIT License |
|
||||
| pyrsistent | 0.19.3 | MIT License |
|
||||
| pytest | 7.2.2 | MIT License |
|
||||
| six | 1.16.0 | MIT License |
|
||||
| tomli | 2.0.1 | MIT License |
|
||||
|
||||
This list was generated at 2023-03-22.
|
||||
@@ -0,0 +1,108 @@
|
||||
active_modules:
|
||||
iso15118_car:
|
||||
module: JsCarV2G
|
||||
config_implementation:
|
||||
main:
|
||||
stack_implementation: RISE-V2G
|
||||
mqtt_base_path: everest_external/iso15118/ev
|
||||
device: auto
|
||||
connector_1:
|
||||
module: EvseManager
|
||||
config_module:
|
||||
connector_id: 1
|
||||
three_phases: true
|
||||
has_ventilation: true
|
||||
country_code: DE
|
||||
rcd_enabled: true
|
||||
evse_id: '1'
|
||||
connections:
|
||||
bsp:
|
||||
- module_id: yeti_driver
|
||||
implementation_id: board_support
|
||||
powermeter_grid_side:
|
||||
- module_id: yeti_driver
|
||||
implementation_id: powermeter
|
||||
yeti_driver:
|
||||
module: JsYetiSimulator
|
||||
slac:
|
||||
module: JsSlacSimulator
|
||||
car_simulator:
|
||||
module: JsCarSimulator
|
||||
config_module:
|
||||
auto_enable: true
|
||||
connector_id: 1
|
||||
auto_enable: true
|
||||
connections:
|
||||
simulation_control:
|
||||
- module_id: yeti_driver
|
||||
implementation_id: yeti_simulation_control
|
||||
ev:
|
||||
- module_id: iso15118_car
|
||||
implementation_id: ev
|
||||
slac:
|
||||
- module_id: slac
|
||||
implementation_id: ev
|
||||
ocpp:
|
||||
module: OCPP
|
||||
config_module:
|
||||
ChargePointConfigPath: ocpp16-config.json
|
||||
UserConfigPath: user_config.json
|
||||
EnableExternalWebsocketControl: true
|
||||
connections:
|
||||
evse_manager:
|
||||
- module_id: connector_1
|
||||
implementation_id: evse
|
||||
reservation:
|
||||
- module_id: auth
|
||||
implementation_id: reservation
|
||||
auth:
|
||||
- module_id: auth
|
||||
implementation_id: main
|
||||
system:
|
||||
- module_id: system
|
||||
implementation_id: main
|
||||
auth:
|
||||
module: Auth
|
||||
config_module:
|
||||
connection_timeout: 20
|
||||
connections:
|
||||
token_provider:
|
||||
- module_id: token_provider_manual
|
||||
implementation_id: main
|
||||
- module_id: ocpp
|
||||
implementation_id: auth_provider
|
||||
token_validator:
|
||||
- module_id: ocpp
|
||||
implementation_id: auth_validator
|
||||
evse_manager:
|
||||
- module_id: connector_1
|
||||
implementation_id: evse
|
||||
token_provider_manual:
|
||||
module: JsDummyTokenProviderManual
|
||||
connections: {}
|
||||
config_implementation:
|
||||
main:
|
||||
token: '123'
|
||||
type: dummy
|
||||
energy_manager:
|
||||
module: EnergyManager
|
||||
connections:
|
||||
energy_trunk:
|
||||
- module_id: grid_connection_point
|
||||
implementation_id: energy_grid
|
||||
grid_connection_point:
|
||||
module: EnergyNode
|
||||
config_module:
|
||||
fuse_limit_A: 63.0
|
||||
phase_count: 3
|
||||
connections:
|
||||
price_information: []
|
||||
energy_consumer:
|
||||
- module_id: connector_1
|
||||
implementation_id: energy_grid
|
||||
powermeter:
|
||||
- module_id: yeti_driver
|
||||
implementation_id: powermeter
|
||||
system:
|
||||
module: System
|
||||
x-module-layout: {}
|
||||
@@ -0,0 +1,171 @@
|
||||
active_modules:
|
||||
iso15118_charger:
|
||||
module: PyJosev
|
||||
config_module:
|
||||
device: auto
|
||||
supported_DIN70121: false
|
||||
iso15118_car:
|
||||
module: JsCarV2G
|
||||
config_implementation:
|
||||
main:
|
||||
stack_implementation: RISE-V2G
|
||||
mqtt_base_path: everest_external/iso15118/ev
|
||||
device: auto
|
||||
connector_1:
|
||||
module: EvseManager
|
||||
config_module:
|
||||
connector_id: 1
|
||||
three_phases: true
|
||||
has_ventilation: true
|
||||
country_code: DE
|
||||
rcd_enabled: true
|
||||
evse_id: "1"
|
||||
session_logging: true
|
||||
session_logging_xml: false
|
||||
ac_hlc_enabled: false
|
||||
ac_hlc_use_5percent: false
|
||||
ac_enforce_hlc: false
|
||||
connections:
|
||||
bsp:
|
||||
- module_id: yeti_driver_1
|
||||
implementation_id: board_support
|
||||
powermeter_grid_side:
|
||||
- module_id: yeti_driver_1
|
||||
implementation_id: powermeter
|
||||
slac:
|
||||
- module_id: slac
|
||||
implementation_id: evse
|
||||
hlc:
|
||||
- module_id: iso15118_charger
|
||||
implementation_id: charger
|
||||
connector_2:
|
||||
module: EvseManager
|
||||
config_module:
|
||||
connector_id: 2
|
||||
three_phases: true
|
||||
has_ventilation: true
|
||||
country_code: DE
|
||||
rcd_enabled: true
|
||||
evse_id: "2"
|
||||
session_logging: true
|
||||
session_logging_xml: false
|
||||
ac_hlc_enabled: false
|
||||
ac_hlc_use_5percent: false
|
||||
ac_enforce_hlc: false
|
||||
connections:
|
||||
bsp:
|
||||
- module_id: yeti_driver_2
|
||||
implementation_id: board_support
|
||||
powermeter_grid_side:
|
||||
- module_id: yeti_driver_2
|
||||
implementation_id: powermeter
|
||||
slac:
|
||||
- module_id: slac
|
||||
implementation_id: evse
|
||||
hlc:
|
||||
- module_id: iso15118_charger
|
||||
implementation_id: charger
|
||||
yeti_driver_1:
|
||||
module: JsYetiSimulator
|
||||
yeti_driver_2:
|
||||
module: JsYetiSimulator
|
||||
slac:
|
||||
module: JsSlacSimulator
|
||||
car_simulator_1:
|
||||
module: JsCarSimulator
|
||||
config_module:
|
||||
connector_id: 1
|
||||
auto_enable: true
|
||||
auto_exec: false
|
||||
auto_exec_commands: sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 30;unplug
|
||||
connections:
|
||||
simulation_control:
|
||||
- module_id: yeti_driver_1
|
||||
implementation_id: yeti_simulation_control
|
||||
ev:
|
||||
- module_id: iso15118_car
|
||||
implementation_id: ev
|
||||
slac:
|
||||
- module_id: slac
|
||||
implementation_id: ev
|
||||
car_simulator_2:
|
||||
module: JsCarSimulator
|
||||
config_module:
|
||||
connector_id: 2
|
||||
auto_enable: true
|
||||
auto_exec: false
|
||||
connections:
|
||||
simulation_control:
|
||||
- module_id: yeti_driver_2
|
||||
implementation_id: yeti_simulation_control
|
||||
ev:
|
||||
- module_id: iso15118_car
|
||||
implementation_id: ev
|
||||
slac:
|
||||
- module_id: slac
|
||||
implementation_id: ev
|
||||
ocpp:
|
||||
module: OCPP201
|
||||
config_module:
|
||||
ChargePointConfigPath: config.json
|
||||
connections:
|
||||
evse_manager:
|
||||
- module_id: connector_1
|
||||
implementation_id: evse
|
||||
- module_id: connector_2
|
||||
implementation_id: evse
|
||||
system:
|
||||
- module_id: system
|
||||
implementation_id: main
|
||||
auth:
|
||||
module: Auth
|
||||
config_module:
|
||||
connection_timeout: 30
|
||||
selection_algorithm: PlugEvents
|
||||
connections:
|
||||
token_provider:
|
||||
- module_id: ocpp
|
||||
implementation_id: auth_provider
|
||||
- module_id: token_provider_manual
|
||||
implementation_id: main
|
||||
token_validator:
|
||||
- module_id: ocpp
|
||||
implementation_id: auth_validator
|
||||
evse_manager:
|
||||
- module_id: connector_1
|
||||
implementation_id: evse
|
||||
- module_id: connector_2
|
||||
implementation_id: evse
|
||||
token_provider_manual:
|
||||
module: JsDummyTokenProviderManual
|
||||
energy_manager:
|
||||
module: EnergyManager
|
||||
connections:
|
||||
energy_trunk:
|
||||
- module_id: grid_connection_point
|
||||
implementation_id: energy_grid
|
||||
grid_connection_point:
|
||||
module: EnergyNode
|
||||
config_module:
|
||||
fuse_limit_A: 40.0
|
||||
phase_count: 3
|
||||
connections:
|
||||
price_information: []
|
||||
energy_consumer:
|
||||
- module_id: connector_1
|
||||
implementation_id: energy_grid
|
||||
- module_id: connector_2
|
||||
implementation_id: energy_grid
|
||||
powermeter:
|
||||
- module_id: yeti_driver_1
|
||||
implementation_id: powermeter
|
||||
api:
|
||||
module: API
|
||||
connections:
|
||||
evse_manager:
|
||||
- module_id: connector_1
|
||||
implementation_id: evse
|
||||
system:
|
||||
module: System
|
||||
|
||||
x-module-layout: {}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"Internal": {
|
||||
"ChargePointId": "cp001",
|
||||
"CentralSystemURI": "127.0.0.1:9000/cp001",
|
||||
"ChargeBoxSerialNumber": "cp001",
|
||||
"ChargePointModel": "Yeti",
|
||||
"ChargePointVendor": "Pionix",
|
||||
"FirmwareVersion": "0.1",
|
||||
"LogMessages": true
|
||||
},
|
||||
"Core": {
|
||||
"AllowOfflineTxForUnknownId": true,
|
||||
"AuthorizeRemoteTxRequests": true,
|
||||
"AuthorizationCacheEnabled": true,
|
||||
"ClockAlignedDataInterval": 900,
|
||||
"ConnectionTimeOut": 10,
|
||||
"ConnectorPhaseRotation": "0.RST,1.RST",
|
||||
"GetConfigurationMaxKeys": 100,
|
||||
"HeartbeatInterval": 86400,
|
||||
"LocalAuthorizeOffline": false,
|
||||
"LocalPreAuthorize": false,
|
||||
"MeterValuesAlignedData": "Energy.Active.Import.Register",
|
||||
"MeterValuesSampledData": "Energy.Active.Import.Register",
|
||||
"MeterValueSampleInterval": 0,
|
||||
"NumberOfConnectors": 1,
|
||||
"ResetRetries": 1,
|
||||
"StopTransactionOnEVSideDisconnect": true,
|
||||
"StopTransactionOnInvalidId": true,
|
||||
"StopTxnAlignedData": "Energy.Active.Import.Register",
|
||||
"StopTxnSampledData": "Energy.Active.Import.Register",
|
||||
"SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging",
|
||||
"TransactionMessageAttempts": 3,
|
||||
"TransactionMessageRetryInterval": 1,
|
||||
"UnlockConnectorOnEVSideDisconnect": true
|
||||
},
|
||||
"FirmwareManagement": {
|
||||
"SupportedFileTransferProtocols": "FTP"
|
||||
},
|
||||
"Security": {
|
||||
"AuthorizationKey": "AABBCCDDEEFFGGHH",
|
||||
"SecurityProfile": 0,
|
||||
"CpoName": "Pionix",
|
||||
"AdditionalRootCertificateCheck": false
|
||||
},
|
||||
"LocalAuthListManagement": {
|
||||
"LocalAuthListEnabled": true,
|
||||
"LocalAuthListMaxLength": 42,
|
||||
"SendLocalListMaxLength": 42
|
||||
},
|
||||
"Reservation": {
|
||||
"ReserveConnectorZeroSupported": true
|
||||
},
|
||||
"SmartCharging": {
|
||||
"ChargeProfileMaxStackLevel": 42,
|
||||
"ChargingScheduleAllowedChargingRateUnit": "Current,Power",
|
||||
"ChargingScheduleMaxPeriods": 42,
|
||||
"MaxChargingProfilesInstalled": 42
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import shutil
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("--everest-prefix", action="store", default="~/checkout/everest-workspace/EVerest",
|
||||
help="EVerest path; default = '~/checkout/everest-workspace/EVerest'")
|
||||
parser.addoption("--libocpp", action="store", default="~/checkout/everest-workspace/libocpp",
|
||||
help="libocpp path; default = '~/checkout/everest-workspace/libocpp'")
|
||||
|
||||
def pytest_configure(config):
|
||||
everest_prefix = config.getoption("--everest-prefix")
|
||||
shutil.copy("conf/ocpp16-config.json", f"{everest_prefix}/share/everest/modules/OCPP")
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
from ocpp.v201 import call_result, call
|
||||
from ocpp.v201.datatypes import SetVariableResultType, IdTokenType
|
||||
from ocpp.v201.enums import SetVariableStatusType, IdTokenType as IdTokenTypeEnum, ClearCacheStatusType, ConnectorStatusType, RequestStartStopStatusType
|
||||
|
||||
from everest.testing.core_utils.controller.everest_test_controller import EverestTestController
|
||||
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
||||
from everest.testing.core_utils.fixtures import *
|
||||
# noinspection PyUnresolvedReferences
|
||||
from everest.testing.ocpp_utils.fixtures import test_utility, charge_point_v16, central_system, test_config, ocpp_config, charge_point, ocpp_version
|
||||
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, OcppTestConfiguration, TestUtility
|
||||
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
|
||||
from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16
|
||||
|
||||
|
||||
def validate_status_notification_201(meta_data, msg, exp_payload):
|
||||
return msg.payload['connectorStatus'] == exp_payload.connector_status and \
|
||||
msg.payload['evseId'] == exp_payload.evse_id and \
|
||||
msg.payload['connectorId'] == exp_payload.connector_id
|
||||
|
||||
|
||||
@ pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp1.6")
|
||||
@pytest.mark.everest_core_config("conf/everest-config-ocpp16.yaml")
|
||||
async def test_ocpp_16(test_config: OcppTestConfiguration, charge_point_v16: ChargePoint16, test_controller: TestController, test_utility: TestUtility):
|
||||
await charge_point_v16.get_configuration_req(key=["AuthorizeRemoteTxRequests"])
|
||||
await charge_point_v16.change_configuration_req(key="MeterValueSampleInterval", value="10")
|
||||
|
||||
# send RemoteStartTransaction.req
|
||||
await charge_point_v16.remote_start_transaction_req(id_tag="DEADBEEF", connector_id=1)
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "RequestStartTransactio", call_result.RequestStartTransactionPayload(status=RequestStartStopStatusType.accepted))
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StatusNotification", {"connectorId": 1, "status": "Preparing"})
|
||||
|
||||
test_controller.plug_in()
|
||||
|
||||
# expect StartTransaction.req
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StartTransaction", {
|
||||
"connectorId": 1, "idTag": "DEADBEEF", "meterStart": 0})
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StatusNotification", {"connectorId": 1, "status": "Charging"})
|
||||
|
||||
assert await charge_point_v16.remote_stop_transaction_req(transaction_id=1)
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "RemoteStopTransaction", {"status": "Accepted"})
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StopTransaction", {"reason": "Remote"})
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StatusNotification", {"connectorId": 1, "status": "Finishing"})
|
||||
|
||||
test_controller.plug_out()
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v16, "StatusNotification", {"connectorId": 1, "status": "Available"})
|
||||
|
||||
|
||||
@ pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.0.1")
|
||||
@pytest.mark.everest_core_config("conf/everest-config-ocpp201.yaml")
|
||||
async def test_ocpp_201(charge_point_v201: ChargePoint201, test_controller: EverestTestController, test_utility: TestUtility):
|
||||
"""This test case tests some requirements around AuthorizationCache of OCPP2.0.1
|
||||
|
||||
Args:
|
||||
charge_point_v201 (ChargePoint201): this fixture starts up a OCPP2.0.1 CSMS and EVerest connection to this CSMS using OCPP. The reference can be used to send and receive messages over OCPP
|
||||
test_controller (TestController): this fixture is used to control the simulation
|
||||
test_utility (TestUtility): this fixture carries meta data of the test case that can be used for validations
|
||||
"""
|
||||
|
||||
# prepare data for the test
|
||||
evse_id = 1
|
||||
connector_id = 1
|
||||
|
||||
# Enable AuthCacheCtrlr
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCacheCtrlr", "Enabled", "true")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
# Enable LocalPreAuthorize
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCtrlr", "LocalPreAuthorize", "true")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
# Set AuthCacheLifeTime
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCacheCtrlr", "LifeTime", "86400")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
# Clear cache
|
||||
r: call_result.ClearCachePayload = await charge_point_v201.clear_cache_req()
|
||||
assert r.status == ClearCacheStatusType.accepted
|
||||
|
||||
test_controller.swipe("DEADBEEF")
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "Authorize", call.AuthorizePayload(
|
||||
id_token=IdTokenType(
|
||||
id_token="DEADBEEF",
|
||||
type=IdTokenTypeEnum.iso14443
|
||||
)
|
||||
))
|
||||
|
||||
test_controller.plug_in()
|
||||
# eventType=Started
|
||||
await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"})
|
||||
test_utility.messages.clear()
|
||||
test_controller.plug_out()
|
||||
# eventType=Ended
|
||||
await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Ended"})
|
||||
|
||||
test_utility.messages.clear()
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# because LocalPreAuthorize is true we dont expect an Authorize.req this time
|
||||
test_utility.forbidden_actions.append("Authorize")
|
||||
|
||||
test_controller.swipe("DEADBEEF")
|
||||
test_controller.plug_in()
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "StatusNotification",
|
||||
call.StatusNotificationPayload(datetime.now().isoformat(),
|
||||
ConnectorStatusType.occupied, evse_id, connector_id),
|
||||
validate_status_notification_201)
|
||||
|
||||
# because LocalPreAuthorize is true we dont expect an authorize here
|
||||
r: call.TransactionEventPayload = call.TransactionEventPayload(**await wait_for_and_validate(test_utility, charge_point_v201,
|
||||
"TransactionEvent", {"eventType": "Started"}))
|
||||
|
||||
# Disable LocalPreAuthorize
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCtrlr", "LocalPreAuthorize", "false")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
# Set AuthCacheLifeTime to 1s
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCacheCtrlr", "LifeTime", "1")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
test_utility.messages.clear()
|
||||
test_controller.plug_out()
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "StatusNotification",
|
||||
call.StatusNotificationPayload(datetime.now().isoformat(),
|
||||
ConnectorStatusType.available, evse_id, connector_id),
|
||||
validate_status_notification_201)
|
||||
|
||||
# eventType=Ended
|
||||
await wait_for_and_validate(test_utility, charge_point_v201,"TransactionEvent", {"eventType": "Ended"})
|
||||
|
||||
test_utility.forbidden_actions.clear()
|
||||
|
||||
test_controller.swipe("DEADBEEF")
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "Authorize", call.AuthorizePayload(
|
||||
id_token=IdTokenType(
|
||||
id_token="DEADBEEF",
|
||||
type=IdTokenTypeEnum.iso14443
|
||||
)
|
||||
))
|
||||
|
||||
# Enable LocalPreAuthorize
|
||||
r: call_result.SetVariablesPayload = await charge_point_v201.set_config_variables_req("AuthCtrlr", "LocalPreAuthorize", "true")
|
||||
set_variable_result: SetVariableResultType = SetVariableResultType(
|
||||
**r.set_variable_result[0])
|
||||
assert set_variable_result.attribute_status == SetVariableStatusType.accepted
|
||||
|
||||
test_controller.plug_in()
|
||||
# eventType=Started
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "TransactionEvent", {"eventType": "Started"})
|
||||
test_utility.messages.clear()
|
||||
test_controller.plug_out()
|
||||
# eventType=Ended
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201,"TransactionEvent", {"eventType": "Ended"})
|
||||
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "StatusNotification",
|
||||
call.StatusNotificationPayload(datetime.now().isoformat(),
|
||||
ConnectorStatusType.available, evse_id, connector_id),
|
||||
validate_status_notification_201)
|
||||
|
||||
# AuthCacheLifeTime expired so sending Authorize.req
|
||||
test_controller.swipe("DEADBEEF")
|
||||
assert await wait_for_and_validate(test_utility, charge_point_v201, "Authorize", call.AuthorizePayload(
|
||||
id_token=IdTokenType(
|
||||
id_token="DEADBEEF",
|
||||
type=IdTokenTypeEnum.iso14443
|
||||
)
|
||||
))
|
||||
@@ -0,0 +1,9 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.autopep8]
|
||||
max_line_length = 120
|
||||
@@ -0,0 +1,34 @@
|
||||
[metadata]
|
||||
name = everest-testing
|
||||
version = attr: everest.testing.__version__
|
||||
author = Piet Gömpel
|
||||
author_email = piet.goempel@pionix.de
|
||||
description = Utilities for testing EVerest
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
url = https://github.com/EVerest/everest-utils
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
|
||||
[options]
|
||||
install_requires =
|
||||
pytest ~=9.0
|
||||
pytest-asyncio ~=1.3
|
||||
python-dateutil ~=2.9
|
||||
paho-mqtt >=2.0
|
||||
pyftpdlib ~=2.2
|
||||
ocpp ==2.1.0
|
||||
websockets ~=13.1
|
||||
pyOpenSSL >=23.2
|
||||
pyyaml ~=6.0
|
||||
cryptography ~=41.0
|
||||
pytest-xdist ~=3.0
|
||||
pytest-timeout ~=2.0
|
||||
pytest-html ~=4.0
|
||||
|
||||
package_dir =
|
||||
= src
|
||||
|
||||
python_requires = >=3.8
|
||||
@@ -0,0 +1 @@
|
||||
__version__ = '0.7.3'
|
||||
@@ -0,0 +1,8 @@
|
||||
from ._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy # flake8: noqa
|
||||
from ._configuration.libocpp_configuration_helper import OCPPConfigAdjustmentStrategy, \
|
||||
OCPPConfigAdjustmentStrategyWrapper # flake8: noqa
|
||||
from .network_isolation import NetworkIsolationStrategy, NetworkIsolationPlugin, \
|
||||
get_worker_interface, WORKER_INTERFACE_ENV # flake8: noqa
|
||||
|
||||
__all__ = ["common", "everest_core", "fixtures", "network_isolation", "probe_module"]
|
||||
@@ -0,0 +1,4 @@
|
||||
"""
|
||||
See base class `EverestConfigAdjustmentStrategy`
|
||||
|
||||
"""
|
||||
@@ -0,0 +1,17 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class EverestConfigAdjustmentStrategy(ABC):
|
||||
""" Strategy that manipulates a (parsed) EVerest config when called.
|
||||
|
||||
Used to build up / adapt EVerest configurations for tests.
|
||||
|
||||
Adjustments can be collected during the configuration setup process. The Everst core class then applies all
|
||||
configuration adjustments.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def adjust_everest_configuration(self, config: Dict) -> Dict:
|
||||
""" Adjusts the provided configuration by making a (deep) copy and returning the adjusted configuration. """
|
||||
pass
|
||||
@@ -0,0 +1,89 @@
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, asdict
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
from everest.testing.core_utils._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
|
||||
|
||||
@dataclass
|
||||
class EvseSecurityModuleConfiguration:
|
||||
csms_ca_bundle: Optional[str] = None
|
||||
mf_ca_bundle: Optional[str] = None
|
||||
mo_ca_bundle: Optional[str] = None
|
||||
v2g_ca_bundle: Optional[str] = None
|
||||
csms_leaf_cert_directory: Optional[str] = None
|
||||
csms_leaf_key_directory: Optional[str] = None
|
||||
secc_leaf_cert_directory: Optional[str] = None
|
||||
secc_leaf_key_directory: Optional[str] = None
|
||||
private_key_password: Optional[str] = None
|
||||
|
||||
|
||||
class EvseSecurityModuleConfigurationStrategy(EverestConfigAdjustmentStrategy):
|
||||
""" Adjusts the Evse security module configuration in the Everest configuration merging a provided configuration into it (if provided) and adapt
|
||||
all paths relative to a certificate target directory (if provided).
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
configuration: Optional[EvseSecurityModuleConfiguration] = None,
|
||||
target_certificates_directory: Optional[Path] = None,
|
||||
source_certificates_directory: Optional[Path] = None,
|
||||
module_id: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
|
||||
Args:
|
||||
configuration: module configuration. If provided. this will be merged into the template configuration (meaning None values are ignored/taken from the originally provided Everest configuration)
|
||||
module_id: Id of security module; if None, auto-detected by module type "EvseSecurity"
|
||||
target_certificates_directory: If provided, all configured certificate directories/paths will be changed to point into this folder
|
||||
source_certificates_directory: If provided, configured certificate directories/paths will be considered relative to this directory; each relative part is appended to the corresponding target paths
|
||||
"""
|
||||
self._security_module_id = module_id
|
||||
self._configuration = configuration
|
||||
self._target_certificates_directory = target_certificates_directory
|
||||
self._source_certificates_directory = source_certificates_directory
|
||||
|
||||
def _move_paths(self, module_config: Dict):
|
||||
def _move(p: Union[str, Path]):
|
||||
if self._source_certificates_directory and Path(p).is_relative_to(self._source_certificates_directory):
|
||||
p = Path(p).relative_to(self._source_certificates_directory)
|
||||
return str(self._target_certificates_directory / p)
|
||||
|
||||
for k in {"csms_ca_bundle",
|
||||
"mf_ca_bundle",
|
||||
"mo_ca_bundle",
|
||||
"v2g_ca_bundle",
|
||||
"csms_leaf_cert_directory",
|
||||
"csms_leaf_key_directory",
|
||||
"secc_leaf_cert_directory",
|
||||
"secc_leaf_key_directory"}:
|
||||
if module_config["config_module"].get(k):
|
||||
module_config["config_module"][k] = _move(module_config["config_module"][k])
|
||||
|
||||
def _determine_module_id(self, everest_config: Dict):
|
||||
if self._security_module_id:
|
||||
assert self._security_module_id in everest_config[
|
||||
"active_modules"], f"Module id {self._security_module_id} not found in EVerest configuration"
|
||||
return self._security_module_id
|
||||
else:
|
||||
try:
|
||||
return next(k for k, v in everest_config["active_modules"].items() if v["module"] == "EvseSecurity")
|
||||
except StopIteration:
|
||||
raise ValueError("No EvseSecurity module found in EVerest configuration")
|
||||
|
||||
def adjust_everest_configuration(self, everest_config: Dict):
|
||||
|
||||
adjusted_config = deepcopy(everest_config)
|
||||
|
||||
module_cfg = adjusted_config["active_modules"][self._determine_module_id(adjusted_config)]
|
||||
|
||||
if self._configuration:
|
||||
module_cfg["config_module"] = {**module_cfg["config_module"],
|
||||
**{k:v for k,v in asdict(self._configuration).items()
|
||||
if v is not None}}
|
||||
|
||||
if self._target_certificates_directory:
|
||||
self._move_paths(module_cfg)
|
||||
|
||||
return adjusted_config
|
||||
@@ -0,0 +1,37 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict
|
||||
|
||||
from everest.testing.core_utils._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
|
||||
|
||||
class EverestMqttConfigurationAdjustmentStrategy(EverestConfigAdjustmentStrategy):
|
||||
""" Adjusts the Everest configuration by manipulating the "settings" block to use the prober Everest UUID and
|
||||
external prefix.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, everest_uuid: str, mqtt_external_prefix: str):
|
||||
self._everest_uuid = everest_uuid
|
||||
self._mqtt_external_prefix = mqtt_external_prefix
|
||||
|
||||
def _find_jscarv2g_module_ids(self, config: Dict):
|
||||
return [k for k, v in config["active_modules"].items()
|
||||
if v.get("module") == "JsCarV2G"]
|
||||
|
||||
def adjust_everest_configuration(self, config: Dict) -> Dict:
|
||||
adjusted_everest_config = deepcopy(config)
|
||||
adjusted_everest_config["settings"] = {}
|
||||
adjusted_everest_config["settings"]["mqtt_everest_prefix"] = f"everest_{self._everest_uuid}"
|
||||
adjusted_everest_config["settings"]["mqtt_external_prefix"] = self._mqtt_external_prefix
|
||||
adjusted_everest_config["settings"]["telemetry_prefix"] = f"telemetry_{self._everest_uuid}"
|
||||
|
||||
# make sure controller starts with a dynamic port
|
||||
adjusted_everest_config["settings"]["controller_port"] = 0
|
||||
|
||||
for car_module_id in self._find_jscarv2g_module_ids(adjusted_everest_config):
|
||||
adjusted_everest_config["active_modules"][car_module_id]\
|
||||
.setdefault("config_implementation",{})\
|
||||
.setdefault("main", {})["mqtt_prefix"] = self._mqtt_external_prefix
|
||||
|
||||
return adjusted_everest_config
|
||||
@@ -0,0 +1,65 @@
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, asdict
|
||||
from typing import Union, Dict
|
||||
|
||||
from everest.testing.core_utils.common import OCPPVersion
|
||||
from everest.testing.core_utils._configuration.everest_configuration_strategies.everest_configuration_strategy import EverestConfigAdjustmentStrategy
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCPPModuleConfigurationBase:
|
||||
MessageLogPath: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCPPModulePaths16(OCPPModuleConfigurationBase):
|
||||
ChargePointConfigPath: str
|
||||
UserConfigPath: str
|
||||
DatabasePath: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class OCPPModulePaths2X(OCPPModuleConfigurationBase):
|
||||
DeviceModelConfigPath: str
|
||||
CoreDatabasePath: str
|
||||
DeviceModelDatabasePath: str
|
||||
EverestDeviceModelDatabasePath: str
|
||||
|
||||
class OCPPModuleConfigurationStrategy(EverestConfigAdjustmentStrategy):
|
||||
""" Adjusts the Everest configuration by manipulating the OCPP module configuration to use proper (temporary test) paths.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ocpp_paths: Union[OCPPModulePaths16, OCPPModulePaths2X],
|
||||
ocpp_module_id: str,
|
||||
ocpp_version: OCPPVersion):
|
||||
self._ocpp_paths = ocpp_paths
|
||||
self._ocpp_module_id = ocpp_module_id
|
||||
self._ocpp_version = ocpp_version
|
||||
|
||||
def adjust_everest_configuration(self, everest_config: Dict):
|
||||
""" Changes the provided configuration of the Everest "OCPP" module .
|
||||
|
||||
Creates the TEST_LOGS_DIR if not existent
|
||||
"""
|
||||
|
||||
adjusted_config = deepcopy(everest_config)
|
||||
|
||||
self._verify_module_config(adjusted_config)
|
||||
|
||||
module_config = adjusted_config["active_modules"][self._ocpp_module_id]
|
||||
|
||||
module_config["config_module"] = {**module_config["config_module"],
|
||||
**asdict(self._ocpp_paths)}
|
||||
|
||||
return adjusted_config
|
||||
|
||||
def _verify_module_config(self, everest_config):
|
||||
""" Verify the provided config fits the provided OCPP version """
|
||||
assert "active_modules" in everest_config and self._ocpp_module_id in everest_config[
|
||||
"active_modules"], "OCPP Module is missing from EVerest config"
|
||||
ocpp_module = everest_config["active_modules"][self._ocpp_module_id]["module"]
|
||||
assert (ocpp_module == "OCPP" and self._ocpp_version == OCPPVersion.ocpp16) or (
|
||||
ocpp_module == "OCPP201" and (self._ocpp_version == OCPPVersion.ocpp201 or
|
||||
self._ocpp_version == OCPPVersion.ocpp21)), \
|
||||
f"Invalid OCCP Module {ocpp_module} for provided OCCP version {self._ocpp_version}"
|
||||
@@ -0,0 +1,46 @@
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
from everest.testing.core_utils._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
|
||||
|
||||
class PersistentStoreConfigurationStrategy(EverestConfigAdjustmentStrategy):
|
||||
""" Adjusts the Everest configuration by manipulating the PersistentStore module configuration to point
|
||||
to the desired (temporary) storage
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
sqlite_db_file_path: Path,
|
||||
module_id: Optional[str] = None):
|
||||
"""
|
||||
|
||||
Args:
|
||||
sqlite_db_file_path: database to be used in configuration
|
||||
module_id: Id of security module; if None, auto-detected by module type "EvseSecurity
|
||||
"""
|
||||
self._module_id = module_id
|
||||
self._sqlite_db_file_path = sqlite_db_file_path
|
||||
|
||||
def _determine_module_id(self, everest_config: Dict):
|
||||
if self._module_id:
|
||||
assert self._module_id in everest_config[
|
||||
"active_modules"], f"Module id {self._module_id} not found in EVerest configuration"
|
||||
return self._module_id
|
||||
else:
|
||||
try:
|
||||
return next(k for k, v in everest_config["active_modules"].items() if v["module"] == "PersistentStore")
|
||||
except StopIteration:
|
||||
raise ValueError("No PersistentStore module found in EVerest configuration")
|
||||
|
||||
def adjust_everest_configuration(self, everest_config: Dict):
|
||||
|
||||
adjusted_config = deepcopy(everest_config)
|
||||
|
||||
module_cfg = adjusted_config["active_modules"][self._determine_module_id(adjusted_config)]
|
||||
|
||||
module_cfg.setdefault("config_module", {})["sqlite_db_file_path"] = str(self._sqlite_db_file_path)
|
||||
|
||||
return adjusted_config
|
||||
@@ -0,0 +1,33 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
|
||||
from everest.testing.core_utils.common import Requirement
|
||||
from everest.testing.core_utils._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
|
||||
|
||||
class ProbeModuleConfigurationStrategy(EverestConfigAdjustmentStrategy):
|
||||
""" Adjusts the Everest configuration by adding the probe module into an EVerest config """
|
||||
|
||||
def __init__(self,
|
||||
connections: Dict[str, List[Requirement]],
|
||||
module_id: str = "probe"
|
||||
):
|
||||
self._module_id = module_id
|
||||
self._connections = connections
|
||||
|
||||
def adjust_everest_configuration(self, everest_config: Dict) -> Dict:
|
||||
adjusted_config = deepcopy(everest_config)
|
||||
|
||||
probe_connections = {
|
||||
requirement_id: [{"module_id": requirement.module_id, "implementation_id": requirement.implementation_id}
|
||||
for requirement in requirements_list]
|
||||
for requirement_id, requirements_list in self._connections.items()}
|
||||
|
||||
active_modules = adjusted_config.setdefault("active_modules", {})
|
||||
active_modules[self._module_id] = {
|
||||
'connections': probe_connections,
|
||||
'module': 'ProbeModule'
|
||||
}
|
||||
|
||||
return adjusted_config
|
||||
@@ -0,0 +1,286 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Union
|
||||
|
||||
import yaml
|
||||
|
||||
from everest.testing.core_utils.common import OCPPVersion
|
||||
from everest.testing.core_utils.everest_core import EverestCore, Requirement
|
||||
from .everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
from .everest_configuration_strategies.evse_security_configuration_strategy import \
|
||||
EvseSecurityModuleConfigurationStrategy, EvseSecurityModuleConfiguration
|
||||
from .everest_configuration_strategies.ocpp_module_configuration_strategy import \
|
||||
OCPPModuleConfigurationStrategy, \
|
||||
OCPPModulePaths16, OCPPModulePaths2X
|
||||
from .everest_configuration_strategies.persistent_store_configuration_strategy import \
|
||||
PersistentStoreConfigurationStrategy
|
||||
from .everest_configuration_strategies.probe_module_configuration_strategy import \
|
||||
ProbeModuleConfigurationStrategy
|
||||
from .libocpp_configuration_helper import \
|
||||
LibOCPP2XConfigurationHelper, LibOCPP16ConfigurationHelper
|
||||
|
||||
|
||||
@dataclass
|
||||
class EverestEnvironmentOCPPConfiguration:
|
||||
ocpp_version: OCPPVersion
|
||||
central_system_port: int
|
||||
central_system_host: str = "127.0.0.1"
|
||||
ocpp_module_id: str = "ocpp"
|
||||
template_ocpp_config: Optional[
|
||||
Path] = None # Path for OCPP config to be used; if not provided, will be determined from everest config
|
||||
device_model_component_config_path: Optional[
|
||||
Path] = None # Path of the OCPP device model json schemas.
|
||||
configuration_strategies: list[OCPPModuleConfigurationStrategy] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EverestEnvironmentEvseSecurityConfiguration:
|
||||
# if true, configuration will be adapted to use temporary certifcates folder, this assumes a "default" file tree structure, cf. the EvseSecurityModuleConfiguration dataclass
|
||||
use_temporary_certificates_folder: bool = True
|
||||
module_id: Optional[str] = None # if None, auto-detected
|
||||
source_certificate_directory: Optional[
|
||||
Path] = None # if provided, this will be copied to temporary path; If none, the certificates of the EVerest directory / installation will be used
|
||||
module_configuration: Optional[
|
||||
# if provided, will be merged into configuration; paths will be adapted if use_temporary_certificates_folder is true
|
||||
EvseSecurityModuleConfiguration] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class EverestEnvironmentPersistentStoreConfiguration:
|
||||
# if true, a temporary persistent storage folder will be used
|
||||
use_temporary_folder: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class EverestEnvironmentCoreConfiguration:
|
||||
everest_core_path: Path
|
||||
template_everest_config_path: Union[
|
||||
Path, None] # Underlying EVerest configuration; will be copied temporarily by EverestCore and adjusted;
|
||||
# if none, config is auto-detected by EVerest
|
||||
|
||||
|
||||
@dataclass
|
||||
class EverestEnvironmentProbeModuleConfiguration:
|
||||
connections: Dict[str, List[Requirement]] = field(default_factory=dict)
|
||||
module_id: str = "probe"
|
||||
|
||||
|
||||
class EverestTestEnvironmentSetup:
|
||||
"""
|
||||
Class that prepares the environment of EVerest core and creates the EverestCore instance of a test.
|
||||
|
||||
For this:
|
||||
- receives all settings / configurations required at initialization
|
||||
- calling setup_environment:
|
||||
- creates required temporary paths / file structures
|
||||
- configures EverestCore including adjustments of the EverestCore configuration (injecting temporary paths ...)
|
||||
- sets up special modules (initiates the setup of OCPPlib such as parsing the device model database)
|
||||
- creates the EverestCore instance
|
||||
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class _EverestEnvironmentTemporaryPaths:
|
||||
""" Paths of the temporary configuration files / data """
|
||||
certs_dir: Path # used by both OCPP and evse security
|
||||
ocpp_config_path: Path
|
||||
ocpp_config_file: Path
|
||||
ocpp_user_config_file: Path
|
||||
ocpp_database_dir: Path
|
||||
ocpp_message_log_directory: Path
|
||||
persistent_store_db_path: Path
|
||||
|
||||
def __init__(self,
|
||||
core_config: EverestEnvironmentCoreConfiguration,
|
||||
ocpp_config: Optional[EverestEnvironmentOCPPConfiguration] = None,
|
||||
probe_config: Optional[EverestEnvironmentProbeModuleConfiguration] = None,
|
||||
evse_security_config: Optional[EverestEnvironmentEvseSecurityConfiguration] = None,
|
||||
persistent_store_config: Optional[EverestEnvironmentPersistentStoreConfiguration] = None,
|
||||
standalone_module: Optional[Union[str, List[str]]] = None,
|
||||
everest_config_strategies: Optional[List[EverestConfigAdjustmentStrategy]] = None
|
||||
) -> None:
|
||||
self._core_config = core_config
|
||||
self._ocpp_config = ocpp_config
|
||||
self._probe_config = probe_config
|
||||
self._evse_security_config = evse_security_config
|
||||
self._persistent_store_config = persistent_store_config
|
||||
self._standalone_module = standalone_module
|
||||
if not self._standalone_module and self._probe_config:
|
||||
self._standalone_module = self._probe_config.module_id
|
||||
self._additional_everest_config_strategies = everest_config_strategies if everest_config_strategies else []
|
||||
self._everest_core = None
|
||||
self._ocpp_configuration = None
|
||||
|
||||
def setup_environment(self, tmp_path: Path):
|
||||
|
||||
temporary_paths = self._create_temporary_directory_structure(tmp_path)
|
||||
|
||||
configuration_strategies = self._create_everest_configuration_strategies(
|
||||
temporary_paths)
|
||||
|
||||
self._everest_core = EverestCore(self._core_config.everest_core_path,
|
||||
self._core_config.template_everest_config_path,
|
||||
everest_configuration_adjustment_strategies=configuration_strategies +
|
||||
self._additional_everest_config_strategies,
|
||||
standalone_module=self._standalone_module,
|
||||
tmp_path=tmp_path)
|
||||
|
||||
if self._ocpp_config:
|
||||
self._ocpp_configuration = self._setup_libocpp_configuration(
|
||||
temporary_paths=temporary_paths
|
||||
)
|
||||
|
||||
if self._evse_security_config:
|
||||
self._setup_evse_security_configuration(temporary_paths)
|
||||
|
||||
@property
|
||||
def everest_core(self) -> EverestCore:
|
||||
assert self._everest_core, "Everest Core not initialized; run 'setup_environment' first"
|
||||
return self._everest_core
|
||||
|
||||
@property
|
||||
def ocpp_config(self):
|
||||
return self._ocpp_configuration
|
||||
|
||||
def _create_temporary_directory_structure(self, tmp_path: Path) -> _EverestEnvironmentTemporaryPaths:
|
||||
ocpp_config_dir = tmp_path / "ocpp_config"
|
||||
ocpp_config_dir.mkdir(exist_ok=True)
|
||||
if self._ocpp_config and self._ocpp_config.ocpp_version == OCPPVersion.ocpp201:
|
||||
component_config_path_standardized = ocpp_config_dir / \
|
||||
"component_config" / "standardized"
|
||||
component_config_path_custom = ocpp_config_dir / "component_config" / "custom"
|
||||
component_config_path_standardized.mkdir(
|
||||
parents=True, exist_ok=True)
|
||||
component_config_path_custom.mkdir(parents=True, exist_ok=True)
|
||||
certs_dir = tmp_path / "certs"
|
||||
certs_dir.mkdir(exist_ok=True)
|
||||
ocpp_logs_dir = ocpp_config_dir / "logs"
|
||||
ocpp_logs_dir.mkdir(exist_ok=True)
|
||||
|
||||
persistent_store_dir = tmp_path / "persistent_storage"
|
||||
persistent_store_dir.mkdir(exist_ok=True)
|
||||
|
||||
logging.info(f"temp ocpp config files directory: {ocpp_config_dir}")
|
||||
|
||||
return self._EverestEnvironmentTemporaryPaths(
|
||||
ocpp_config_path=ocpp_config_dir / "component_config",
|
||||
ocpp_config_file=ocpp_config_dir / "config.json",
|
||||
ocpp_user_config_file=ocpp_config_dir / "user_config.json",
|
||||
ocpp_database_dir=ocpp_config_dir,
|
||||
certs_dir=certs_dir,
|
||||
ocpp_message_log_directory=ocpp_logs_dir,
|
||||
persistent_store_db_path=persistent_store_dir / "persistent_store.db"
|
||||
)
|
||||
|
||||
def _create_ocpp_module_configuration_strategy(self,
|
||||
temporary_paths: _EverestEnvironmentTemporaryPaths) -> OCPPModuleConfigurationStrategy:
|
||||
|
||||
if self._ocpp_config.ocpp_version == OCPPVersion.ocpp16:
|
||||
ocpp_paths = OCPPModulePaths16(
|
||||
ChargePointConfigPath=str(temporary_paths.ocpp_config_file),
|
||||
MessageLogPath=str(temporary_paths.ocpp_message_log_directory),
|
||||
UserConfigPath=str(temporary_paths.ocpp_user_config_file),
|
||||
DatabasePath=str(temporary_paths.ocpp_database_dir)
|
||||
)
|
||||
elif self._ocpp_config.ocpp_version == OCPPVersion.ocpp201 or self._ocpp_config.ocpp_version == OCPPVersion.ocpp21:
|
||||
ocpp_paths = OCPPModulePaths2X(
|
||||
DeviceModelConfigPath=str(temporary_paths.ocpp_config_path),
|
||||
MessageLogPath=str(temporary_paths.ocpp_message_log_directory),
|
||||
CoreDatabasePath=str(temporary_paths.ocpp_database_dir),
|
||||
DeviceModelDatabasePath=str(temporary_paths.ocpp_database_dir / "device_model_storage.db"),
|
||||
EverestDeviceModelDatabasePath=str(temporary_paths.ocpp_database_dir / "everest_device_model_storage.db")
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"unknown ocpp version {self._ocpp_config.ocpp_version}")
|
||||
|
||||
occp_module_configuration_helper = OCPPModuleConfigurationStrategy(ocpp_paths=ocpp_paths,
|
||||
ocpp_module_id=self._ocpp_config.ocpp_module_id,
|
||||
ocpp_version=self._ocpp_config.ocpp_version)
|
||||
|
||||
return occp_module_configuration_helper
|
||||
|
||||
def _setup_libocpp_configuration(self, temporary_paths: _EverestEnvironmentTemporaryPaths):
|
||||
|
||||
liboccp_configuration_helper = LibOCPP16ConfigurationHelper(
|
||||
) if self._ocpp_config.ocpp_version == OCPPVersion.ocpp16 else LibOCPP2XConfigurationHelper()
|
||||
|
||||
if self._ocpp_config.template_ocpp_config:
|
||||
source_ocpp_config = self._ocpp_config.template_ocpp_config
|
||||
elif self._ocpp_config.ocpp_version == OCPPVersion.ocpp16:
|
||||
source_ocpp_config = self._determine_configured_charge_point_config_path_from_everest_config()
|
||||
elif self._ocpp_config.ocpp_version == OCPPVersion.ocpp201 or self._ocpp_config.ocpp_version == OCPPVersion.ocpp21:
|
||||
source_ocpp_config = self._ocpp_config.device_model_component_config_path
|
||||
|
||||
return liboccp_configuration_helper.generate_ocpp_config(
|
||||
central_system_port=self._ocpp_config.central_system_port,
|
||||
central_system_host=self._ocpp_config.central_system_host,
|
||||
source_ocpp_config_path=source_ocpp_config,
|
||||
target_ocpp_config_path=temporary_paths.ocpp_config_file
|
||||
if self._ocpp_config.ocpp_version == OCPPVersion.ocpp16
|
||||
else temporary_paths.ocpp_config_path,
|
||||
target_ocpp_user_config_file=temporary_paths.ocpp_user_config_file,
|
||||
configuration_strategies=self._ocpp_config.configuration_strategies
|
||||
)
|
||||
|
||||
def _create_everest_configuration_strategies(self, temporary_paths: _EverestEnvironmentTemporaryPaths):
|
||||
configuration_strategies = []
|
||||
if self._ocpp_config:
|
||||
configuration_strategies.append(
|
||||
self._create_ocpp_module_configuration_strategy(temporary_paths))
|
||||
if self._probe_config:
|
||||
configuration_strategies.append(
|
||||
ProbeModuleConfigurationStrategy(connections=self._probe_config.connections,
|
||||
module_id=self._probe_config.module_id))
|
||||
|
||||
if self._evse_security_config:
|
||||
configuration_strategies.append(
|
||||
EvseSecurityModuleConfigurationStrategy(module_id=self._evse_security_config.module_id,
|
||||
configuration=self._evse_security_config.module_configuration,
|
||||
source_certificates_directory=self._evse_security_config.source_certificate_directory,
|
||||
target_certificates_directory=temporary_paths.certs_dir
|
||||
if self._evse_security_config.use_temporary_certificates_folder
|
||||
else None
|
||||
))
|
||||
|
||||
if self._persistent_store_config and self._persistent_store_config.use_temporary_folder:
|
||||
configuration_strategies.append(
|
||||
PersistentStoreConfigurationStrategy(
|
||||
sqlite_db_file_path=temporary_paths.persistent_store_db_path)
|
||||
)
|
||||
|
||||
return configuration_strategies
|
||||
|
||||
def _determine_configured_charge_point_config_path_from_everest_config(self):
|
||||
|
||||
if self._ocpp_config.ocpp_version == OCPPVersion.ocpp16:
|
||||
everest_template_config = yaml.safe_load(
|
||||
self._core_config.template_everest_config_path.read_text())
|
||||
|
||||
charge_point_config_path = \
|
||||
everest_template_config["active_modules"][self._ocpp_config.ocpp_module_id]["config_module"][
|
||||
"ChargePointConfigPath"]
|
||||
|
||||
ocpp_dir = self._everest_core.prefix_path / "share/everest/modules/OCPP"
|
||||
else:
|
||||
raise ValueError(f"Could not determine ChargePointConfigPath for OCPP version {self._ocpp_config.ocpp_version}")
|
||||
ocpp_config_path = ocpp_dir / charge_point_config_path
|
||||
return ocpp_config_path
|
||||
|
||||
def _setup_evse_security_configuration(self, temporary_paths: _EverestEnvironmentTemporaryPaths):
|
||||
""" If configures, copies the source certificate trees"""
|
||||
if self._evse_security_config.source_certificate_directory:
|
||||
source_certs_directory = self._evse_security_config.source_certificate_directory
|
||||
else:
|
||||
source_certs_directory = self._everest_core.etc_path / 'certs'
|
||||
logging.warning(
|
||||
"No 'source_certificate_directory' configured in EverestEnvironmentEvseSecurityConfiguration. "
|
||||
f"Will use certificates from local installation {source_certs_directory}', which might lead to flaky tests.")
|
||||
shutil.copytree(source_certs_directory,
|
||||
temporary_paths.certs_dir, dirs_exist_ok=True)
|
||||
@@ -0,0 +1,215 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from glob import glob
|
||||
import json
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Union, Callable
|
||||
|
||||
|
||||
class OCPPConfigAdjustmentStrategy(ABC):
|
||||
""" Strategy that manipulates a OCPP config when called. Cf. EverestConfigurationAdjustmentStrategy class
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def adjust_ocpp_configuration(self, config: dict) -> dict:
|
||||
""" Adjusts the provided configuration by making a (deep) copy and returning the adjusted configuration. """
|
||||
|
||||
|
||||
class OCPPConfigAdjustmentStrategyWrapper(OCPPConfigAdjustmentStrategy):
|
||||
""" Simple OCPPConfigAdjustmentStrategy from a callback function.
|
||||
"""
|
||||
|
||||
def __init__(self, callback: Callable[[dict], dict]):
|
||||
self._callback = callback
|
||||
|
||||
def adjust_ocpp_configuration(self, config: dict) -> dict:
|
||||
""" Adjusts the provided configuration by making a (deep) copy and returning the adjusted configuration. """
|
||||
config = deepcopy(config)
|
||||
return self._callback(config)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OCPP2XConfigVariableIdentifier:
|
||||
component_name: str
|
||||
variable_name: str
|
||||
variable_attribute_type: str = "Actual"
|
||||
|
||||
|
||||
class GenericOCPP16ConfigAdjustment(OCPPConfigAdjustmentStrategy):
|
||||
""" Generic OCPPConfigAdjustmentStrategy for OCPP 1.6 that allows simple variable value adjustments.
|
||||
|
||||
use e.g. via marker
|
||||
@pytest.mark.ocpp_config_adaptions(GenericOCPP16ConfigAdjustment([("Custom", "ExampleConfigurationKey", "test_value")]))
|
||||
"""
|
||||
|
||||
def __init__(self, adjustments: list[tuple[str, str, Any]]):
|
||||
self._adjustments = adjustments
|
||||
|
||||
def adjust_ocpp_configuration(self, config: dict):
|
||||
config = copy.deepcopy(config)
|
||||
for (category, variable, value) in self._adjustments:
|
||||
config.setdefault(category, {})[variable] = value
|
||||
return config
|
||||
|
||||
|
||||
class GenericOCPP2XConfigAdjustment(OCPPConfigAdjustmentStrategy):
|
||||
""" Generic OCPPConfigAdjustmentStrategy for OCPP 2.X that allows simple variable value adjustments.
|
||||
|
||||
use e.g. via marker
|
||||
@pytest.mark.ocpp_config_adaptions(GenericOCPP2XConfigAdjustment([(OCPP2XConfigVariableIdentifier("CustomCntrlr","TestVariableName", "Actual"), "test_value")]))
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, adjustments: list[tuple[OCPP2XConfigVariableIdentifier, Any]]):
|
||||
self._adjustments = adjustments
|
||||
|
||||
def adjust_ocpp_configuration(self, config: dict):
|
||||
config = copy.deepcopy(config)
|
||||
for identifier, value in self._adjustments:
|
||||
self._set_value_in_v2_config(config, identifier, value)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def _get_value_from_v2_config(ocpp_config: dict, identifier: OCPP2XConfigVariableIdentifier):
|
||||
for (component, schema) in ocpp_config.items():
|
||||
if component == identifier.component_name:
|
||||
attributes = schema["properties"][identifier.variable_name]["attributes"]
|
||||
for attribute in attributes:
|
||||
if attribute["type"] == identifier.variable_attribute_type:
|
||||
return attribute["value"]
|
||||
|
||||
@staticmethod
|
||||
def _set_value_in_v2_config(ocpp_config: dict, identifier: OCPP2XConfigVariableIdentifier,
|
||||
value: Any):
|
||||
for (component, schema) in ocpp_config.items():
|
||||
if component == identifier.component_name:
|
||||
attributes = schema["properties"][identifier.variable_name]["attributes"]
|
||||
for attribute in attributes:
|
||||
if attribute["type"] == identifier.variable_attribute_type:
|
||||
attribute["value"] = value
|
||||
|
||||
|
||||
class _OCPP2XNetworkConnectionProfileAdjustment(OCPPConfigAdjustmentStrategy):
|
||||
""" Adjusts the OCPP 2.X Network Connection Profile by injecting the right host, port and chargepoint id.
|
||||
|
||||
This is utilized by the `LibOCPP2XConfigurationHelper`.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, central_system_port: int | str = None, central_system_host: str = None, security_profile: int = None):
|
||||
self._central_system_port = central_system_port
|
||||
self._central_system_host = central_system_host
|
||||
self._security_profile = security_profile
|
||||
|
||||
def adjust_ocpp_configuration(self, config: dict):
|
||||
config = deepcopy(config)
|
||||
network_connection_profiles = json.loads(GenericOCPP2XConfigAdjustment._get_value_from_v2_config(
|
||||
config, OCPP2XConfigVariableIdentifier("InternalCtrlr", "NetworkConnectionProfiles", "Actual")))
|
||||
for network_connection_profile in network_connection_profiles:
|
||||
selected_security_profile = network_connection_profile["connectionData"][
|
||||
"securityProfile"] if self._security_profile is None else self._security_profile
|
||||
selected_central_system_port = network_connection_profile["connectionData"][
|
||||
"ocppCsmsUrl"] if self._central_system_port is None else self._central_system_port
|
||||
selected_central_system_host = network_connection_profile["connectionData"][
|
||||
"ocppCsmsUrl"] if self._central_system_host is None else self._central_system_host
|
||||
protocol = "ws" if selected_security_profile == 1 else "wss"
|
||||
network_connection_profile["connectionData"][
|
||||
"ocppCsmsUrl"] = f"{protocol}://{selected_central_system_host}:{selected_central_system_port}"
|
||||
network_connection_profile["connectionData"][
|
||||
"securityProfile"] = selected_security_profile
|
||||
GenericOCPP2XConfigAdjustment._set_value_in_v2_config(config, OCPP2XConfigVariableIdentifier("InternalCtrlr", "NetworkConnectionProfiles",
|
||||
"Actual"), json.dumps(network_connection_profiles))
|
||||
return config
|
||||
|
||||
|
||||
class LibOCPPConfigurationHelperBase(ABC):
|
||||
""" Helper for parsing / adapting the LibOCPP configuration and dumping it a database file. """
|
||||
|
||||
def generate_ocpp_config(self,
|
||||
target_ocpp_config_path: Path,
|
||||
target_ocpp_user_config_file: Path,
|
||||
source_ocpp_config_path: Path,
|
||||
central_system_host: str,
|
||||
central_system_port: Union[str, int],
|
||||
configuration_strategies: list[OCPPConfigAdjustmentStrategy] | None = None):
|
||||
config = self._get_config(source_ocpp_config_path)
|
||||
|
||||
configuration_strategies = configuration_strategies if configuration_strategies else []
|
||||
|
||||
for v in [self._get_default_strategy(central_system_port, central_system_host)] + configuration_strategies:
|
||||
config = v.adjust_ocpp_configuration(config)
|
||||
|
||||
self._store_config(config, target_ocpp_config_path)
|
||||
target_ocpp_user_config_file.write_text("{}")
|
||||
|
||||
return config
|
||||
|
||||
@abstractmethod
|
||||
def _get_config(self, source_ocpp_config_path: Path):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _get_default_strategy(self, central_system_port: int | str,
|
||||
central_system_host: str) -> OCPPConfigAdjustmentStrategy:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _store_config(self, config, target_ocpp_config_file):
|
||||
pass
|
||||
|
||||
|
||||
class LibOCPP16ConfigurationHelper(LibOCPPConfigurationHelperBase):
|
||||
def _get_config(self, source_ocpp_config_path: Path):
|
||||
return json.loads(source_ocpp_config_path.read_text())
|
||||
|
||||
def _get_default_strategy(self, central_system_port, central_system_host):
|
||||
def adjust_ocpp_configuration(config: dict) -> dict:
|
||||
config = deepcopy(config)
|
||||
charge_point_id = config["Internal"]["ChargePointId"]
|
||||
config["Internal"][
|
||||
"CentralSystemURI"] = f"{central_system_host}:{central_system_port}/{charge_point_id}"
|
||||
return config
|
||||
|
||||
return OCPPConfigAdjustmentStrategyWrapper(adjust_ocpp_configuration)
|
||||
|
||||
def _store_config(self, config, target_ocpp_config_file):
|
||||
with target_ocpp_config_file.open("w") as f:
|
||||
json.dump(config, f)
|
||||
|
||||
|
||||
class LibOCPP2XConfigurationHelper(LibOCPPConfigurationHelperBase):
|
||||
|
||||
def _get_config(self, source_ocpp_config_path: Path):
|
||||
config = {}
|
||||
file_list_standardized = glob(
|
||||
str(source_ocpp_config_path / "standardized" / "*.json"), recursive=False)
|
||||
file_list_custom = glob(
|
||||
str(source_ocpp_config_path / "custom" / "*.json"), recursive=False)
|
||||
file_list = file_list_standardized + file_list_custom
|
||||
for file in file_list:
|
||||
# Get component from file name
|
||||
_, tail = os.path.split(file)
|
||||
component_name, _ = os.path.splitext(tail)
|
||||
# Store json in dict
|
||||
with open(file) as f:
|
||||
config[component_name] = json.load(f)
|
||||
return config
|
||||
|
||||
def _get_default_strategy(self, central_system_port: int | str,
|
||||
central_system_host: str) -> OCPPConfigAdjustmentStrategy:
|
||||
return _OCPP2XNetworkConnectionProfileAdjustment(central_system_port, central_system_host)
|
||||
|
||||
def _store_config(self, config, target_ocpp_config_path):
|
||||
# Just store all in the 'standardized' folder
|
||||
path = target_ocpp_config_path / "standardized"
|
||||
for key, value in config.items():
|
||||
file_name = path / (key + '.json')
|
||||
file_name.parent.mkdir(parents=True, exist_ok=True)
|
||||
with file_name.open("w+") as f:
|
||||
json.dump(value, f)
|
||||
@@ -0,0 +1,13 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Requirement:
|
||||
def __init__(self, module_id: str, implementation_id: str):
|
||||
self.module_id = module_id
|
||||
self.implementation_id = implementation_id
|
||||
|
||||
|
||||
class OCPPVersion(str, Enum):
|
||||
ocpp16 = "ocpp1.6"
|
||||
ocpp201 = "ocpp2.0.1"
|
||||
ocpp21 = "ocpp2.1"
|
||||
@@ -0,0 +1,156 @@
|
||||
import json
|
||||
import os
|
||||
import paho.mqtt.client as mqtt
|
||||
from paho.mqtt import __version__ as paho_mqtt_version
|
||||
|
||||
from everest.testing.core_utils.everest_core import EverestCore
|
||||
|
||||
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
||||
|
||||
|
||||
class EverestTestController(TestController):
|
||||
|
||||
def __init__(self,
|
||||
everest_core: EverestCore
|
||||
):
|
||||
self._everest_core = everest_core
|
||||
self._mqtt_client = None
|
||||
|
||||
@property
|
||||
def _mqtt_external_prefix(self):
|
||||
return self._everest_core.mqtt_external_prefix
|
||||
|
||||
def start(self):
|
||||
self._initialize_external_mqtt_client()
|
||||
self._everest_core.start()
|
||||
self._initialize_nodered_sil()
|
||||
|
||||
def stop(self, *exc_details):
|
||||
self._everest_core.stop()
|
||||
self._destroy_mqtt_client()
|
||||
|
||||
def _initialize_external_mqtt_client(self):
|
||||
mqtt_server_uri = os.environ.get("MQTT_SERVER_ADDRESS", "127.0.0.1")
|
||||
mqtt_server_port = int(os.environ.get("MQTT_SERVER_PORT", "1883"))
|
||||
if paho_mqtt_version < '2.0':
|
||||
self._mqtt_client = mqtt.Client(self._everest_core.everest_uuid)
|
||||
else:
|
||||
self._mqtt_client = mqtt.Client(
|
||||
callback_api_version=mqtt.CallbackAPIVersion.VERSION2, client_id=self._everest_core.everest_uuid)
|
||||
self._mqtt_client.connect(mqtt_server_uri, mqtt_server_port)
|
||||
|
||||
def _initialize_nodered_sil(self):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/1/carsim/cmd/enable", "true")
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/2/carsim/cmd/enable", "true")
|
||||
|
||||
def plug_in(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session",
|
||||
"sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 32,1;sleep 200;unplug")
|
||||
|
||||
def plug_in_ac_iso(self, connector_id=1, payment_type=""):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session",
|
||||
f"sleep 1;iso_wait_slac_matched;iso_start_v2g_session AC {payment_type} 86400 0;iso_wait_pwr_ready;iso_draw_power_regulated 16,3;sleep 60;iso_stop_charging;iso_wait_v2g_session_stopped;unplug")
|
||||
|
||||
def plug_in_dc_iso(self, connector_id=1, payment_type=""):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session",
|
||||
f"sleep 1;iso_wait_slac_matched;iso_start_v2g_session DC {payment_type} 86400 0;iso_wait_pwr_ready;iso_wait_for_stop 60;iso_wait_v2g_session_stopped;unplug"
|
||||
)
|
||||
|
||||
def plug_out(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"unplug")
|
||||
|
||||
def plug_out_iso(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"iso_stop_charging;iso_wait_v2g_session_stopped;unplug")
|
||||
|
||||
def pause_session(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"pause;sleep 36000"
|
||||
)
|
||||
|
||||
def resume_session(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"draw_power_regulated 16,3"
|
||||
)
|
||||
|
||||
def pause_iso_session(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"iso_pause_charging;iso_wait_for_resume"
|
||||
)
|
||||
|
||||
def resume_iso_session_ac(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"iso_start_bcb_toggle 3;iso_wait_pwm_is_running;iso_start_v2g_session AC 86400 0;iso_wait_pwr_ready;iso_draw_power_regulated 16,3;iso_wait_for_stop 60"
|
||||
)
|
||||
|
||||
def resume_iso_session_dc(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/modify_charging_session",
|
||||
"iso_start_bcb_toggle 3;iso_wait_pwm_is_running;iso_start_v2g_session DC 86400 0;iso_wait_pwr_ready;iso_dc_power_on;iso_wait_for_stop 60"
|
||||
)
|
||||
|
||||
def swipe(self, token, connectors=None):
|
||||
connectors = connectors if connectors is not None else [1]
|
||||
provided_token = {
|
||||
"id_token": {
|
||||
"value": token,
|
||||
"type": "ISO14443"
|
||||
},
|
||||
"authorization_type": "RFID",
|
||||
"connectors": connectors
|
||||
}
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_api/dummy_token_provider/cmd/provide", json.dumps(provided_token))
|
||||
|
||||
def connect_websocket(self):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_api/ocpp/cmd/connect", "on")
|
||||
|
||||
def disconnect_websocket(self):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_api/ocpp/cmd/disconnect", "off")
|
||||
|
||||
def diode_fail(self, connector_id=1):
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/cmd/execute_charging_session",
|
||||
"sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 32,3;sleep 5;diode_fail;sleep 36000;unplug")
|
||||
|
||||
def raise_error(self, error_string="MREC6UnderVoltage", connector_id=1):
|
||||
raise_error_payload = {
|
||||
"error_type": error_string,
|
||||
"raise": "true"
|
||||
}
|
||||
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/error",
|
||||
json.dumps(raise_error_payload))
|
||||
|
||||
def clear_error(self, error_string="MREC6UnderVoltage", connector_id=1):
|
||||
clear_error_payload = {
|
||||
"error_type": error_string,
|
||||
"raise": "false"
|
||||
}
|
||||
|
||||
self._mqtt_client.publish(
|
||||
f"{self._mqtt_external_prefix}everest_external/nodered/{connector_id}/carsim/error",
|
||||
json.dumps(clear_error_payload))
|
||||
|
||||
def publish(self, topic, payload):
|
||||
self._mqtt_client.publish(topic, payload)
|
||||
|
||||
def _destroy_mqtt_client(self):
|
||||
if self._mqtt_client:
|
||||
self._mqtt_client.disconnect()
|
||||
self._mqtt_client = None
|
||||
@@ -0,0 +1,121 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
class TestController():
|
||||
|
||||
"""This abstract class defines methods that are used within the test cases
|
||||
and should be implemented by you for your specific chargepoint and test
|
||||
environment. It includes definitions for the simulated behavior and events of a
|
||||
chargepoint and an electric vehicle.
|
||||
"""
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
This method starts the chargepoint. This includes
|
||||
the connection of the OCPP client to the CSMS.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
This method stops the chargepoint (similiar to power off). This includes
|
||||
the disconnection of the OCPP client from the CSMS.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plug_in(self, connector_id):
|
||||
"""
|
||||
Plug in of an electric vehicle to the chargepoint.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plug_in_ac_iso(self, payment_type, connector_id):
|
||||
"""
|
||||
Plug in of an electric vehicle to the chargepoint using AC ISO15118.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plug_in_dc_iso(self, payment_type, connector_id):
|
||||
"""
|
||||
Plug in of an electric vehicle to the chargepoint using DC ISO15118.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plug_out_iso(self, connector_id):
|
||||
"""
|
||||
Plug out of an electric vehicle properly ending the ISO15118 session.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def pause_session(self, connector_id):
|
||||
"""
|
||||
Pause an ongoing charging session.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resume_session(self, connector_id):
|
||||
"""
|
||||
Resume a paused charging session.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def pause_iso_session(self, connector_id):
|
||||
"""
|
||||
Pause an ongoing ISO15118 session initiated by the EV.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resume_iso_session_ac(self, connector_id):
|
||||
"""
|
||||
Resume a paused ISO15118 session initiated by the EV.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resume_iso_session_dc(self, connector_id):
|
||||
"""
|
||||
Resume a paused ISO15118 session initiated by the EV.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def plug_out(self):
|
||||
"""
|
||||
Plug out of an electric vehicle from the chargepoint.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def swipe(self, token):
|
||||
"""
|
||||
Swipe the given RFID card at the RFID reader of the chargepoint.
|
||||
"""
|
||||
|
||||
|
||||
def connect_websocket(self):
|
||||
"""
|
||||
Connect the OCPP client. This method is only used after a disconnect_websocket call
|
||||
in the tests.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def disconnect_websocket(self):
|
||||
"""
|
||||
Disconnects the OCPP client from the CSMS. The chargepoint still is powered up.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def didoe_fail(self):
|
||||
"""
|
||||
Produces an RCD Error.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def raise_error(self, error_string, connector_id):
|
||||
"""
|
||||
Produces an error (default MREC6UnderVoltage).
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def clear_error(self, error_string, connector_id):
|
||||
"""
|
||||
Clears an error (default MREC6UnderVoltage).
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,253 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
from threading import Thread
|
||||
import threading
|
||||
import time
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import List, Optional, Union, Dict
|
||||
import uuid
|
||||
import yaml
|
||||
import selectors
|
||||
from signal import SIGINT
|
||||
|
||||
from everest.framework import RuntimeSession
|
||||
from everest.testing.core_utils.common import Requirement
|
||||
from ._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
from ._configuration.everest_configuration_strategies.mqtt_configuration_strategy import \
|
||||
EverestMqttConfigurationAdjustmentStrategy
|
||||
from ._configuration.everest_configuration_strategies.probe_module_configuration_strategy import \
|
||||
ProbeModuleConfigurationStrategy
|
||||
|
||||
STARTUP_TIMEOUT = 30
|
||||
|
||||
Connections = dict[str, List[Requirement]]
|
||||
|
||||
|
||||
class StatusFifoListener:
|
||||
def __init__(self, status_fifo_path: Path):
|
||||
if not status_fifo_path.exists():
|
||||
os.mkfifo(status_fifo_path)
|
||||
|
||||
# note: open doesn't support non-blocking, so we use os.open to get the fd
|
||||
fd = os.open(status_fifo_path, flags=(os.O_RDONLY | os.O_NONBLOCK))
|
||||
self._file_obj = open(fd)
|
||||
|
||||
selector = selectors.DefaultSelector()
|
||||
selector.register(self._file_obj, selectors.EVENT_READ)
|
||||
self._selector = selector
|
||||
|
||||
def wait_for_status(self, timeout: float, match_status: list[str]) -> Optional[list[str]]:
|
||||
if match_status is None:
|
||||
match_status = []
|
||||
|
||||
end_time = time.time() + timeout
|
||||
|
||||
while True:
|
||||
for _key, _mask in self._selector.select(timeout):
|
||||
data = self._file_obj.read()
|
||||
if len(data) == 0:
|
||||
return None
|
||||
|
||||
# plural!
|
||||
received_status = data.splitlines()
|
||||
|
||||
if len(match_status) == 0:
|
||||
# we're not trying to match any messages
|
||||
return received_status
|
||||
|
||||
# return the filtered matched messages
|
||||
matched_status = [status for status in match_status if status in received_status]
|
||||
if len(matched_status) > 0:
|
||||
return matched_status
|
||||
|
||||
timeout = end_time - time.time()
|
||||
|
||||
if timeout < 0:
|
||||
return []
|
||||
|
||||
|
||||
class EverestCore:
|
||||
"""This class can be used to configure, start and stop a full build of EVerest
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
prefix_path: Path,
|
||||
config_path: Path = None,
|
||||
standalone_module: Optional[Union[str, List[str]]] = None,
|
||||
everest_configuration_adjustment_strategies: Optional[
|
||||
List[EverestConfigAdjustmentStrategy]] = None,
|
||||
tmp_path: Optional[Path] = None) -> None:
|
||||
"""Initialize EVerest using everest_core_path and everest_config_path
|
||||
|
||||
Args:
|
||||
everest_prefix (Path): location of installed everest distribution".
|
||||
standalone_module (str): Standalone module parameter provided to EVerest manager app (can be overwritten in startup)
|
||||
"""
|
||||
|
||||
self.process = None
|
||||
self.everest_uuid = uuid.uuid4().hex
|
||||
|
||||
if not tmp_path:
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix=self.everest_uuid))
|
||||
temp_everest_config_file = tempfile.NamedTemporaryFile(
|
||||
delete=False, mode="w+", suffix=".yaml", dir=temp_dir)
|
||||
self.everest_config_path = Path(temp_everest_config_file.name)
|
||||
self.everest_core_user_config_path = Path(
|
||||
temp_everest_config_file.name).parent / 'user-config'
|
||||
self.everest_core_user_config_path.mkdir(parents=True, exist_ok=True)
|
||||
self._status_fifo_path = temp_dir / "status.fifo"
|
||||
else:
|
||||
config_dir = tmp_path / "everest_config"
|
||||
config_dir.mkdir()
|
||||
self.everest_core_user_config_path = config_dir / "user-config"
|
||||
self.everest_core_user_config_path.mkdir()
|
||||
self.everest_config_path = config_dir / "everest_config.yaml"
|
||||
self._status_fifo_path = tmp_path / "status.fifo"
|
||||
|
||||
self.prefix_path = prefix_path
|
||||
self.etc_path = Path('/etc/everest') if prefix_path == '/usr' else prefix_path / 'etc/everest'
|
||||
|
||||
if config_path is None:
|
||||
config_path = self.etc_path / 'config-sil.yaml'
|
||||
|
||||
self.mqtt_external_prefix = f"external_{self.everest_uuid}"
|
||||
|
||||
self._write_temporary_config(config_path, everest_configuration_adjustment_strategies)
|
||||
|
||||
logging.info(f"everest uuid: {self.everest_uuid}")
|
||||
logging.info(f"temp everest config: {self.everest_config_path} based on {config_path}")
|
||||
|
||||
self.test_control_modules = None
|
||||
|
||||
self.log_reader_thread: Thread = None
|
||||
self.everest_running = False
|
||||
self.all_modules_started_event = threading.Event()
|
||||
|
||||
self._standalone_module = standalone_module
|
||||
|
||||
@property
|
||||
def everest_config(self) -> Dict:
|
||||
with self.everest_config_path.open("r") as f:
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def _write_temporary_config(self, template_config_path: Path, everest_configuration_adjustment_strategies: Optional[
|
||||
List[EverestConfigAdjustmentStrategy]]):
|
||||
everest_configuration_adjustment_strategies = everest_configuration_adjustment_strategies if everest_configuration_adjustment_strategies else []
|
||||
everest_configuration_adjustment_strategies.append(
|
||||
EverestMqttConfigurationAdjustmentStrategy(everest_uuid=self.everest_uuid,
|
||||
mqtt_external_prefix=self.mqtt_external_prefix))
|
||||
everest_config = yaml.safe_load(template_config_path.read_text())
|
||||
for strategy in everest_configuration_adjustment_strategies:
|
||||
everest_config = strategy.adjust_everest_configuration(everest_config)
|
||||
with self.everest_config_path.open("w") as f:
|
||||
yaml.dump(everest_config, f)
|
||||
|
||||
def start(self, standalone_module: Optional[Union[str, List[str]]] = None, test_connections: Connections = None):
|
||||
"""Starts EVerest in a subprocess
|
||||
|
||||
Args:
|
||||
standalone_module (str, optional): If set, a submodule can be started separately. EVerest will then wait for the submodule to be started.
|
||||
Defaults to None.
|
||||
"""
|
||||
|
||||
standalone_module = standalone_module if standalone_module is not None else self._standalone_module
|
||||
|
||||
manager_path = self.prefix_path / 'bin/manager'
|
||||
|
||||
logging.info(f'config: {self.everest_config_path}')
|
||||
|
||||
# FIXME (aw): clean up passing of modules_to_test
|
||||
self.test_connections = test_connections if test_connections != None else {}
|
||||
self._create_testing_user_config()
|
||||
|
||||
self.status_listener = StatusFifoListener(self._status_fifo_path)
|
||||
logging.info(self._status_fifo_path)
|
||||
|
||||
args = [str(manager_path.resolve()), '--config', str(self.everest_config_path),
|
||||
'--status-fifo', str(self._status_fifo_path), '--prefix', str(self.prefix_path.resolve())]
|
||||
|
||||
if standalone_module:
|
||||
logging.info(f"Standalone module {standalone_module} was specified")
|
||||
if not isinstance(standalone_module, list):
|
||||
standalone_module = [standalone_module]
|
||||
for s in standalone_module:
|
||||
args.extend(['--standalone', s])
|
||||
|
||||
logging.info(" ".join(args))
|
||||
|
||||
logging.info('Starting EVerest...')
|
||||
logging.info(' '.join(args))
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
args, cwd=self.prefix_path, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
self.log_reader_thread = Thread(target=self.read_everest_log)
|
||||
self.log_reader_thread.start()
|
||||
|
||||
expected_status = 'ALL_MODULES_STARTED' if standalone_module == None else 'WAITING_FOR_STANDALONE_MODULES'
|
||||
|
||||
status = self.status_listener.wait_for_status(STARTUP_TIMEOUT, [expected_status])
|
||||
if status == None or len(status) == 0:
|
||||
self.read_everest_log()
|
||||
raise TimeoutError("Timeout while waiting for EVerest to start")
|
||||
|
||||
logging.info("EVerest has started")
|
||||
if expected_status == 'ALL_MODULES_STARTED':
|
||||
self.all_modules_started_event.set()
|
||||
|
||||
def read_everest_log(self):
|
||||
while self.process.poll() == None:
|
||||
stderr_raw = self.process.stderr.readline()
|
||||
stderr_formatted = stderr_raw.strip().decode(errors="ignore")
|
||||
logging.debug(f' {stderr_formatted}')
|
||||
|
||||
if self.process.returncode == 0:
|
||||
logging.info("EVerest stopped with return code 0")
|
||||
elif self.process.returncode < 0:
|
||||
logging.info(f"EVerest stopped by signal {signal.Signals(-self.process.returncode).name}")
|
||||
else:
|
||||
logging.warning(f"EVerest stopped with return code: {self.process.returncode}")
|
||||
|
||||
logging.debug("EVerest output stopped")
|
||||
|
||||
def stop(self):
|
||||
"""Stops execution of EVerest by signaling SIGINT
|
||||
"""
|
||||
logging.debug("CONTROLLER stop() function called...")
|
||||
if self.process:
|
||||
# NOTE (aw): we could also call process.kill()
|
||||
self.process.send_signal(SIGINT)
|
||||
self.process.wait()
|
||||
|
||||
if self.log_reader_thread:
|
||||
self.log_reader_thread.join()
|
||||
|
||||
def _create_testing_user_config(self):
|
||||
"""Creates a user-config file to include the PyTestControlModule in the current SIL simulation.
|
||||
If a user-config already exists, it will be re-named
|
||||
"""
|
||||
|
||||
if len(self.test_connections) == 0:
|
||||
# nothing to do here
|
||||
return
|
||||
|
||||
file = self.everest_core_user_config_path / self.everest_config_path.name
|
||||
logging.info(f"temp everest user-config: {file.resolve()}")
|
||||
|
||||
# FIXME (aw): we need some agreement, if the module id of the probe module should be fixed or not
|
||||
logging.info(f'Adding test control module(s) to user-config: {self.test_control_modules}')
|
||||
user_config = {}
|
||||
user_config = ProbeModuleConfigurationStrategy(connections=self.test_connections).adjust_everest_configuration(user_config)
|
||||
|
||||
file.write_text(yaml.dump(user_config))
|
||||
|
||||
def get_runtime_session(self):
|
||||
return RuntimeSession(str(self.prefix_path), str(self.everest_config_path))
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import paho.mqtt.client as mqtt
|
||||
from paho.mqtt import __version__ as paho_mqtt_version
|
||||
|
||||
from ._configuration.everest_configuration_strategies.everest_configuration_strategy import \
|
||||
EverestConfigAdjustmentStrategy
|
||||
from ._configuration.everest_environment_setup import \
|
||||
EverestEnvironmentProbeModuleConfiguration, \
|
||||
EverestTestEnvironmentSetup, EverestEnvironmentOCPPConfiguration, EverestEnvironmentCoreConfiguration, \
|
||||
EverestEnvironmentEvseSecurityConfiguration, EverestEnvironmentPersistentStoreConfiguration
|
||||
from everest.testing.core_utils.controller.everest_test_controller import EverestTestController
|
||||
from everest.testing.core_utils.everest_core import EverestCore
|
||||
from everest.testing.core_utils.network_isolation import (
|
||||
NetworkIsolationStrategy,
|
||||
WORKER_INTERFACE_ENV,
|
||||
WORKER_PROXY_INTERFACE_ENV,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def probe_module_config(request) -> Optional[EverestEnvironmentProbeModuleConfiguration]:
|
||||
marker = request.node.get_closest_marker("probe_module")
|
||||
if marker:
|
||||
return EverestEnvironmentProbeModuleConfiguration(
|
||||
**marker.kwargs
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def core_config(request) -> EverestEnvironmentCoreConfiguration:
|
||||
everest_prefix = Path(request.config.getoption("--everest-prefix"))
|
||||
|
||||
marker = request.node.get_closest_marker("everest_core_config")
|
||||
if marker is None:
|
||||
everest_config_path = None # config auto-detected by everest core
|
||||
else:
|
||||
path = Path('/etc/everest') if everest_prefix == '/usr' else everest_prefix / 'etc/everest'
|
||||
everest_config_path = path / marker.args[0]
|
||||
|
||||
return EverestEnvironmentCoreConfiguration(
|
||||
everest_core_path=everest_prefix,
|
||||
template_everest_config_path=everest_config_path,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ocpp_config(request) -> Optional[EverestEnvironmentOCPPConfiguration]:
|
||||
return None
|
||||
|
||||
@pytest.fixture
|
||||
def evse_security_config(request) -> Optional[EverestEnvironmentEvseSecurityConfiguration]:
|
||||
source_certs_dir_marker = request.node.get_closest_marker("source_certs_dir")
|
||||
if source_certs_dir_marker:
|
||||
return EverestEnvironmentEvseSecurityConfiguration(source_certificate_directory=Path(source_certs_dir_marker.args[0]))
|
||||
return None
|
||||
|
||||
@pytest.fixture
|
||||
def persistent_store_config(request) -> Optional[EverestEnvironmentPersistentStoreConfiguration]:
|
||||
persistent_store_marker = request.node.get_closest_marker("use_temporary_persistent_store")
|
||||
if persistent_store_marker:
|
||||
return EverestEnvironmentPersistentStoreConfiguration(use_temporary_folder=True)
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def everest_config_strategies(request) -> list[EverestConfigAdjustmentStrategy]:
|
||||
additional_configuration_strategies = []
|
||||
additional_configuration_strategies_marker = request.node.get_closest_marker('everest_config_adaptions')
|
||||
if additional_configuration_strategies_marker:
|
||||
for v in additional_configuration_strategies_marker.args:
|
||||
assert isinstance(v, EverestConfigAdjustmentStrategy), "Arguments to 'everest_config_adaptions' must all be instances of EverestConfigAdjustmentStrategy"
|
||||
additional_configuration_strategies.append(v)
|
||||
|
||||
# Auto-inject NetworkIsolationStrategy when a worker interface is assigned.
|
||||
# The env vars are set by the NetworkIsolationPlugin on xdist worker nodes.
|
||||
interface = os.environ.get(WORKER_INTERFACE_ENV)
|
||||
if interface:
|
||||
proxy_interface = os.environ.get(WORKER_PROXY_INTERFACE_ENV)
|
||||
additional_configuration_strategies.append(NetworkIsolationStrategy(interface, proxy_interface))
|
||||
|
||||
return additional_configuration_strategies
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def everest_environment(request,
|
||||
tmp_path,
|
||||
core_config: EverestEnvironmentCoreConfiguration,
|
||||
ocpp_config: Optional[EverestEnvironmentOCPPConfiguration],
|
||||
probe_module_config: Optional[EverestEnvironmentProbeModuleConfiguration],
|
||||
evse_security_config: Optional[EverestEnvironmentEvseSecurityConfiguration],
|
||||
persistent_store_config: Optional[EverestEnvironmentPersistentStoreConfiguration],
|
||||
everest_config_strategies
|
||||
):
|
||||
standalone_module_marker = request.node.get_closest_marker('standalone_module')
|
||||
|
||||
environment_setup = EverestTestEnvironmentSetup(
|
||||
core_config=core_config,
|
||||
ocpp_config=ocpp_config,
|
||||
probe_config=probe_module_config,
|
||||
evse_security_config=evse_security_config,
|
||||
persistent_store_config=persistent_store_config,
|
||||
standalone_module=list(standalone_module_marker.args) if standalone_module_marker else None,
|
||||
everest_config_strategies=everest_config_strategies
|
||||
)
|
||||
|
||||
environment_setup.setup_environment(tmp_path=tmp_path)
|
||||
|
||||
yield environment_setup
|
||||
|
||||
@pytest.fixture
|
||||
def everest_core(request,
|
||||
everest_environment
|
||||
)-> EverestCore:
|
||||
"""Fixture that can be used to start and stop EVerest"""
|
||||
|
||||
yield everest_environment.everest_core
|
||||
|
||||
# FIXME (aw): proper life time management, shouldn't the fixure start and stop?
|
||||
everest_environment.everest_core.stop()
|
||||
|
||||
@pytest.fixture
|
||||
def ocpp_configuration(everest_environment):
|
||||
yield everest_environment.ocpp_config
|
||||
|
||||
@pytest.fixture
|
||||
def test_controller(request, tmp_path, everest_core) -> EverestTestController:
|
||||
"""Fixture that references the test_controller that can be used for
|
||||
control events for the test cases.
|
||||
"""
|
||||
|
||||
test_controller = EverestTestController(everest_core=everest_core)
|
||||
|
||||
yield test_controller
|
||||
|
||||
# FIXME (aw): proper life time management, shouldn't the fixure start and stop?
|
||||
test_controller.stop()
|
||||
|
||||
@pytest.fixture
|
||||
def connected_mqtt_client(everest_core: EverestCore) -> mqtt.Client:
|
||||
mqtt_server_uri = os.environ.get("MQTT_SERVER_ADDRESS", "127.0.0.1")
|
||||
mqtt_server_port = int(os.environ.get("MQTT_SERVER_PORT", "1883"))
|
||||
if paho_mqtt_version < '2.0':
|
||||
client = mqtt.Client(everest_core.everest_uuid)
|
||||
else:
|
||||
client = mqtt.Client(
|
||||
callback_api_version=mqtt.CallbackAPIVersion.VERSION1, client_id=everest_core.everest_uuid)
|
||||
client.connect(mqtt_server_uri, mqtt_server_port)
|
||||
client.loop_start()
|
||||
|
||||
yield client
|
||||
|
||||
client.loop_stop()
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
from .strategy import NetworkIsolationStrategy
|
||||
from .plugin import (
|
||||
NetworkIsolationPlugin,
|
||||
get_worker_interface,
|
||||
VETH_PREFIX,
|
||||
WORKER_INTERFACE_ENV,
|
||||
WORKER_PROXY_INTERFACE_ENV,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"NetworkIsolationStrategy",
|
||||
"NetworkIsolationPlugin",
|
||||
"VETH_PREFIX",
|
||||
"get_worker_interface",
|
||||
"WORKER_INTERFACE_ENV",
|
||||
"WORKER_PROXY_INTERFACE_ENV",
|
||||
]
|
||||
@@ -0,0 +1,196 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
"""
|
||||
Pytest plugin for network-isolated parallel ISO 15118 test execution.
|
||||
|
||||
This plugin integrates with pytest-xdist to:
|
||||
1. Detect pre-existing veth pairs (created externally via setup-network-isolation.sh)
|
||||
2. Assign a unique interface to each xdist worker via workerinput
|
||||
3. Strip @pytest.mark.xdist_group(name="ISO15118") markers so those tests
|
||||
can be distributed freely across workers
|
||||
4. Automatically inject a NetworkIsolationStrategy via the everest_config_strategies
|
||||
fixture override in conftest.py
|
||||
|
||||
When veth pairs are NOT available, the plugin is a no-op and the xdist_group
|
||||
markers remain — tests fall back to sequential execution within their group.
|
||||
|
||||
The plugin never creates or destroys network interfaces. That is done externally
|
||||
via `setup-network-isolation.sh` (which requires sudo / CAP_NET_ADMIN).
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
# Naming convention for veth pairs: ev_test0/ev_test0_peer, ev_test1/ev_test1_peer, ...
|
||||
VETH_PREFIX = "ev_test"
|
||||
|
||||
# Environment variables used to pass the assigned interfaces from controller to workers
|
||||
WORKER_INTERFACE_ENV = "EVEREST_TEST_NETWORK_INTERFACE"
|
||||
WORKER_PROXY_INTERFACE_ENV = "EVEREST_TEST_PROXY_NETWORK_INTERFACE"
|
||||
|
||||
# The xdist_group name used by ISO 15118 tests
|
||||
ISO15118_XDIST_GROUP = "ISO15118"
|
||||
|
||||
|
||||
def interface_exists(name: str) -> bool:
|
||||
"""Check if a network interface exists."""
|
||||
result = subprocess.run(
|
||||
["ip", "link", "show", name],
|
||||
capture_output=True, text=True, check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def get_worker_index(worker_id: str) -> Optional[int]:
|
||||
"""Extract numeric index from xdist worker_id (e.g., 'gw0' -> 0).
|
||||
|
||||
Returns None if not running under xdist (worker_id == 'master').
|
||||
"""
|
||||
if worker_id == "master":
|
||||
return None
|
||||
# worker_id format: "gw0", "gw1", etc.
|
||||
return int(worker_id.replace("gw", ""))
|
||||
|
||||
|
||||
def get_worker_interface() -> Optional[str]:
|
||||
"""Get the network interface assigned to the current xdist worker.
|
||||
|
||||
Returns None if network isolation is not active.
|
||||
"""
|
||||
return os.environ.get(WORKER_INTERFACE_ENV)
|
||||
|
||||
|
||||
class NetworkIsolationPlugin:
|
||||
"""Pytest plugin that detects pre-existing veth pairs and enables parallel
|
||||
ISO 15118 test execution.
|
||||
|
||||
Behavior:
|
||||
--network-isolation passed AND veth pairs exist:
|
||||
-> Adopts interfaces, assigns one per worker, strips xdist_group markers
|
||||
--network-isolation passed but NO veth pairs:
|
||||
-> Logs a warning, xdist_group markers stay (sequential fallback)
|
||||
--network-isolation NOT passed:
|
||||
-> Plugin is not registered, everything works as before
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._num_workers: int = 0
|
||||
self._active = False # True only if interfaces were found
|
||||
|
||||
@staticmethod
|
||||
def register(config):
|
||||
"""Register this plugin with pytest if --network-isolation is passed."""
|
||||
if config.getoption("--network-isolation", default=False):
|
||||
plugin = NetworkIsolationPlugin()
|
||||
config.pluginmanager.register(plugin, "network_isolation")
|
||||
|
||||
def pytest_configure_node(self, node):
|
||||
"""Called on the controller for each xdist worker node.
|
||||
|
||||
Passes the assigned interface name via workerinput.
|
||||
"""
|
||||
if not self._active:
|
||||
return
|
||||
worker_id = node.workerinput["workerid"]
|
||||
idx = get_worker_index(worker_id)
|
||||
if idx is not None and idx < self._num_workers:
|
||||
interface = f"{VETH_PREFIX}{idx}"
|
||||
node.workerinput["network_interface"] = interface
|
||||
node.workerinput["proxy_interface"] = f"{interface}_peer"
|
||||
|
||||
@staticmethod
|
||||
def pytest_configure(config):
|
||||
"""On worker nodes, read the assigned interface and set the env var.
|
||||
|
||||
xdist workers have `config.workerinput` populated by the controller's
|
||||
`pytest_configure_node` hook.
|
||||
"""
|
||||
workerinput = getattr(config, "workerinput", None)
|
||||
if workerinput and "network_interface" in workerinput:
|
||||
os.environ[WORKER_INTERFACE_ENV] = workerinput["network_interface"]
|
||||
if workerinput and "proxy_interface" in workerinput:
|
||||
os.environ[WORKER_PROXY_INTERFACE_ENV] = workerinput["proxy_interface"]
|
||||
|
||||
def pytest_sessionstart(self, session):
|
||||
"""Detect and adopt pre-existing veth pairs at session start."""
|
||||
# Determine worker count
|
||||
num_workers = session.config.getoption("numprocesses", default=None)
|
||||
if num_workers is None or num_workers == 0:
|
||||
num_workers = os.cpu_count() or 4
|
||||
if isinstance(num_workers, str):
|
||||
num_workers = os.cpu_count() or 4 if num_workers == "auto" else int(num_workers)
|
||||
|
||||
# Check if interfaces exist (created by setup-network-isolation.sh)
|
||||
first_iface = f"{VETH_PREFIX}0"
|
||||
if not interface_exists(first_iface):
|
||||
logging.warning(
|
||||
"--network-isolation was passed but no veth interfaces found "
|
||||
"(expected %s). ISO 15118 tests will fall back to sequential "
|
||||
"execution via xdist_group. Run 'sudo ./setup-network-isolation.sh "
|
||||
"setup %d' first to enable parallel execution.",
|
||||
first_iface,
|
||||
num_workers,
|
||||
)
|
||||
return
|
||||
|
||||
self._num_workers = num_workers
|
||||
self._active = True
|
||||
|
||||
@staticmethod
|
||||
def _is_iso15118_xdist_marker(marker) -> bool:
|
||||
"""Check if a marker is @pytest.mark.xdist_group(name="ISO15118")."""
|
||||
return (
|
||||
marker.name == "xdist_group"
|
||||
and (
|
||||
marker.kwargs.get("name") == ISO15118_XDIST_GROUP
|
||||
or (marker.args and marker.args[0] == ISO15118_XDIST_GROUP)
|
||||
)
|
||||
)
|
||||
|
||||
def _strip_marker_from_node(self, node) -> bool:
|
||||
"""Remove ISO15118 xdist_group marker from a single node. Returns True if removed."""
|
||||
original_len = len(node.own_markers)
|
||||
node.own_markers = [
|
||||
m for m in node.own_markers
|
||||
if not self._is_iso15118_xdist_marker(m)
|
||||
]
|
||||
return len(node.own_markers) < original_len
|
||||
|
||||
def pytest_collection_modifyitems(self, config, items):
|
||||
"""Remove xdist_group('ISO15118') markers when network isolation is active.
|
||||
|
||||
This allows ISO 15118 tests to be distributed freely across workers
|
||||
instead of being forced into a single sequential group.
|
||||
|
||||
Markers can live on test functions, classes, or modules, so we strip
|
||||
from the item and all its parent nodes.
|
||||
"""
|
||||
if not self._active:
|
||||
return
|
||||
|
||||
processed_parents = set()
|
||||
|
||||
for item in items:
|
||||
# Check if this item inherits an ISO15118 xdist_group marker
|
||||
has_iso_group = any(
|
||||
self._is_iso15118_xdist_marker(m)
|
||||
for m in item.iter_markers("xdist_group")
|
||||
)
|
||||
if not has_iso_group:
|
||||
continue
|
||||
|
||||
# Strip from the item itself
|
||||
self._strip_marker_from_node(item)
|
||||
|
||||
# Strip from parent nodes (class, module) — but only once per parent
|
||||
parent = item.parent
|
||||
while parent is not None:
|
||||
parent_id = id(parent)
|
||||
if parent_id not in processed_parents:
|
||||
processed_parents.add(parent_id)
|
||||
self._strip_marker_from_node(parent)
|
||||
parent = parent.parent
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
"""
|
||||
EverestConfigAdjustmentStrategy that rewrites the `device` field in ISO 15118
|
||||
modules to use a specific network interface, enabling parallel test execution.
|
||||
|
||||
Usage:
|
||||
@pytest.mark.everest_config_adaptions(NetworkIsolationStrategy("ev_test0", "ev_test0_peer"))
|
||||
"""
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Optional
|
||||
|
||||
from everest.testing.core_utils import EverestConfigAdjustmentStrategy
|
||||
|
||||
# EV-facing modules: listen on the EV-side of the veth pair
|
||||
_EV_SIDE_MODULES = frozenset({"PyEvJosev", "IsoMux"})
|
||||
|
||||
# EVSE proxy-side modules: listen on the EVSE/proxy-side of the veth pair
|
||||
_PROXY_SIDE_MODULES = frozenset({"EvseV2G", "Evse15118D20"})
|
||||
|
||||
# All ISO 15118 module types handled by this strategy
|
||||
ISO15118_MODULE_TYPES = _EV_SIDE_MODULES | _PROXY_SIDE_MODULES
|
||||
|
||||
|
||||
class NetworkIsolationStrategy(EverestConfigAdjustmentStrategy):
|
||||
"""Rewrites `device` in ISO 15118 modules to use dedicated network interfaces.
|
||||
|
||||
This enables multiple ISO 15118 test sessions to run in parallel by assigning
|
||||
each test to a separate virtual ethernet (veth) interface pair, avoiding port
|
||||
and multicast conflicts.
|
||||
|
||||
When IsoMux is present:
|
||||
- IsoMux and PyEvJosev use `interface_name` as `device` (EV-facing side).
|
||||
- EvseV2G and Evse15118D20 use `proxy_interface_name` as `device`.
|
||||
- IsoMux `proxy_device` is set to `proxy_interface_name` so it connects
|
||||
to the ISO-2/ISO-20 instances via link-local instead of loopback.
|
||||
|
||||
When IsoMux is absent:
|
||||
- All ISO 15118 modules use `interface_name` as `device`.
|
||||
|
||||
Args:
|
||||
interface_name: The EV-facing network interface (e.g., "ev_test0").
|
||||
proxy_interface_name: The EVSE proxy-facing interface (e.g., "ev_test0_peer").
|
||||
Used only when IsoMux is present in the config.
|
||||
"""
|
||||
|
||||
def __init__(self, interface_name: str, proxy_interface_name: Optional[str] = None):
|
||||
self._interface_name = interface_name
|
||||
self._proxy_interface_name = proxy_interface_name
|
||||
|
||||
def adjust_everest_configuration(self, everest_config: Dict) -> Dict:
|
||||
adjusted_config = deepcopy(everest_config)
|
||||
|
||||
active_modules = adjusted_config.get("active_modules", {})
|
||||
|
||||
has_isomux = any(
|
||||
module_def.get("module") == "IsoMux"
|
||||
for module_def in active_modules.values()
|
||||
)
|
||||
|
||||
for module_def in active_modules.values():
|
||||
module_type = module_def.get("module", "")
|
||||
if module_type not in ISO15118_MODULE_TYPES:
|
||||
continue
|
||||
|
||||
config_module = module_def.get("config_module", {})
|
||||
|
||||
if module_type in _EV_SIDE_MODULES:
|
||||
if "device" in config_module:
|
||||
config_module["device"] = self._interface_name
|
||||
if module_type == "IsoMux" and self._proxy_interface_name:
|
||||
config_module["proxy_device"] = self._proxy_interface_name
|
||||
else: # _PROXY_SIDE_MODULES
|
||||
if "device" in config_module:
|
||||
config_module["device"] = (
|
||||
self._proxy_interface_name
|
||||
if has_isomux and self._proxy_interface_name
|
||||
else self._interface_name
|
||||
)
|
||||
|
||||
return adjusted_config
|
||||
@@ -0,0 +1,178 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from queue import Queue
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from everest.framework import Module, RuntimeSession
|
||||
from everest.framework import error
|
||||
|
||||
class ProbeModule:
|
||||
"""
|
||||
Probe module tool for integration testing, which is a thin abstraction over the C++ bindings from everestpy
|
||||
You need to declare the requirements for the probe module with the fixtures starting EVerest ("test_connections" argument),
|
||||
but you do not need to specify the interfaces provided by the probe - simply specify the implementation ID when registering cmd handlers and publishing vars.
|
||||
"""
|
||||
|
||||
def __init__(self, session: RuntimeSession, module_id="probe"):
|
||||
"""
|
||||
Construct a probe module and connect it to EVerest. This does not mark the module as ready yet.
|
||||
- session: runtime session information (path to EVerest installation and location of run config file)
|
||||
- module_id: the module ID to register with EVerest. By default, this will be "probe".
|
||||
- start: whether to start the module immediately. Set to false if you need to add implementations or subscriptions before starting.
|
||||
"""
|
||||
logging.info("ProbeModule init start")
|
||||
m = Module(module_id, session)
|
||||
self._setup = m.say_hello()
|
||||
self._mod = m
|
||||
self._ready_event = threading.Event()
|
||||
self._started = False
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Send the "ready" signal for the probe module.
|
||||
You should do this after implementing all commands needed in your test.
|
||||
"""
|
||||
if self._started:
|
||||
raise RuntimeError("Called start(), but ProbeModule is started already!")
|
||||
self._started = True
|
||||
# subscribe to session events
|
||||
self._mod.init_done(self._ready)
|
||||
logging.info("Probe module initialized")
|
||||
|
||||
async def call_command(self, connection_id: str, command_name: str, args: dict) -> Any:
|
||||
"""
|
||||
Call a command on another module.
|
||||
- connection_id: the id of the connection, as specified for the probe module in the runtime config
|
||||
- command_name: the name of the command to execute
|
||||
- args: the arguments for the command, as a name->value mapping
|
||||
returns: the result of the command invocation
|
||||
"""
|
||||
|
||||
interface = self._setup.connections[connection_id][0]
|
||||
try:
|
||||
async with asyncio.timeout(30):
|
||||
return await asyncio.to_thread(lambda: self._mod.call_command(interface, command_name, args))
|
||||
except TimeoutError as e:
|
||||
error_message = f"Timeout in calling {connection_id}.{command_name}: {type(e)}: {e}. This might be caused by the other module/EVerest exiting abnormally."
|
||||
logging.error(error_message)
|
||||
raise RuntimeError(error_message)
|
||||
except Exception as e:
|
||||
logging.info(
|
||||
f"Exception in calling {connection_id}.{command_name}: {type(e)}: {e}")
|
||||
|
||||
def implement_command(self, implementation_id: str, command_name: str, handler: Callable[[dict], Any]):
|
||||
"""
|
||||
Set up an implementation for a command.
|
||||
- implementation_id: the id of the implementation, as used by other modules requiring it in the runtime config
|
||||
- command_name: the name of the command to execute
|
||||
- handler: a function to handle the command, which takes a dict of arguments as input, and returns the return value as a dict (json object)
|
||||
Note: The handler runs in a separate thread!
|
||||
|
||||
|
||||
!!! WARNING: UNIMPLEMENTED COMMANDS MAY CAUSE EVEREST TO HANG !!!
|
||||
----
|
||||
In the MQTT-based protocol used by EVerest, commands are initiated by publishing requests to a specific MQTT topic.
|
||||
The implementor is subscribed to this topic, and when they are done executing a command, they publish a result on the same topic.
|
||||
To "implement" a command really just means to subscribe to the command's topic and attach a handler to process incoming requests there.
|
||||
If you do not implement a command, but another module tries to call it anyway, the command request won't reach anyone, and the caller will be stuck forever waiting for a response.
|
||||
If your tests hang, make sure you have implemented all commands which are called in the probe - the EVerest framework does not check this.
|
||||
"""
|
||||
self._mod.implement_command(implementation_id, command_name, handler)
|
||||
|
||||
def publish_variable(self, implementation_id: str, variable_name: str, value: Any):
|
||||
"""
|
||||
Publish a variable from an interface the probe module implements.
|
||||
- implementation_id: the id of the implementation, as used by other modules requiring it in the runtime config
|
||||
- variable_name: the name of the variable
|
||||
- value: the value to publish
|
||||
"""
|
||||
self._mod.publish_variable(implementation_id, variable_name, value)
|
||||
|
||||
def subscribe_variable(self, connection_id: str, variable_name: str, handler: Callable[[dict], None]):
|
||||
"""
|
||||
Subscribe to a variable implemented by a module required by the probe module.
|
||||
- connection_id: the id of the connection, as specified for the probe module in the runtime config
|
||||
- variable_name: the name of the variable
|
||||
- handler: a function to handle incoming values for the variable, accepting a dict as an input, and returning nothing.
|
||||
Note: The handler runs in a separate thread!
|
||||
"""
|
||||
self._mod.subscribe_variable(self._setup.connections[connection_id][0], variable_name, handler)
|
||||
|
||||
def subscribe_variable_to_queue(self, connection_id: str, var_name: str):
|
||||
"""
|
||||
The same as subscribe_variable, but incoming values will be pushed to a queue
|
||||
"""
|
||||
queue = Queue()
|
||||
self._mod.subscribe_variable(self._setup.connections[connection_id][0], var_name,
|
||||
lambda message, _queue=queue: _queue.put(message))
|
||||
return queue
|
||||
|
||||
def raise_error(self, implementation_id: str, error_obj: error.Error):
|
||||
"""
|
||||
Raise an error from an interface the probe module implements.
|
||||
- implementation_id: the id of the implementation, as used by other modules requiring it in the runtime config
|
||||
- error_obj: the Error object to raise
|
||||
"""
|
||||
self._mod.raise_error(implementation_id, error_obj)
|
||||
|
||||
def clear_error(self, implementation_id: str, error_type: str, sub_type: Optional[str] = None):
|
||||
"""
|
||||
Clear an error from an interface the probe module implements.
|
||||
- implementation_id: the id of the implementation, as used by other modules requiring it in the runtime config
|
||||
- error_type: the type of the error to clear
|
||||
- sub_type: optional sub-type of the error to clear
|
||||
"""
|
||||
if sub_type is not None:
|
||||
self._mod.clear_error(implementation_id, error_type, sub_type)
|
||||
else:
|
||||
self._mod.clear_error(implementation_id, error_type)
|
||||
|
||||
def subscribe_error(self, connection_id: str, error_type: str,
|
||||
callback: Callable[[error.Error], None],
|
||||
clear_callback: Callable[[error.Error], None]):
|
||||
"""
|
||||
Subscribe to a specific error type from a module required by the probe module.
|
||||
- connection_id: the id of the connection, as specified for the probe module in the runtime config
|
||||
- error_type: the type of errors to subscribe to
|
||||
- callback: a function to handle when the error is raised, accepting an Error object
|
||||
- clear_callback: a function to handle when the error is cleared, accepting an Error object
|
||||
"""
|
||||
self._mod.subscribe_error(self._setup.connections[connection_id][0], error_type, callback, clear_callback)
|
||||
|
||||
def subscribe_all_errors(self, connection_id: str,
|
||||
callback: Callable[[error.Error], None],
|
||||
clear_callback: Callable[[error.Error], None]):
|
||||
"""
|
||||
Subscribe to all errors from a module required by the probe module.
|
||||
- connection_id: the id of the connection, as specified for the probe module in the runtime config
|
||||
- callback: a function to handle when any error is raised, accepting an Error object
|
||||
- clear_callback: a function to handle when any error is cleared, accepting an Error object
|
||||
"""
|
||||
self._mod.subscribe_all_errors(self._setup.connections[connection_id][0], callback, clear_callback)
|
||||
|
||||
def _ready(self):
|
||||
"""
|
||||
Internal function: callback triggered by the EVerest framework when all modules have been initialized
|
||||
This is equivalent to the ready() method in C++ modules
|
||||
"""
|
||||
self._ready_event.set()
|
||||
|
||||
async def wait_for_event(self, timeout: float):
|
||||
"""
|
||||
Helper to make threading.Event behave similar to asyncio.Event, which is awaitable and raising TimeoutError.
|
||||
- timeout: Time to for ready_event
|
||||
"""
|
||||
self._ready_event.wait(timeout)
|
||||
if not self._ready_event.is_set():
|
||||
raise TimeoutError("Waiting for ready: timeout")
|
||||
|
||||
async def wait_to_be_ready(self, timeout=3.0):
|
||||
"""
|
||||
Convenience method which allows you to wait until the _ready() callback is triggered (i.e. until EVerest is up and running)
|
||||
"""
|
||||
if not self._started:
|
||||
raise RuntimeError("Called wait_to_be_ready(), but probe module has not been started yet! "
|
||||
"Please use start() to start the module first.")
|
||||
await self.wait_for_event(timeout)
|
||||
@@ -0,0 +1,331 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import ssl
|
||||
import time
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import wraps
|
||||
from typing import Union, Optional
|
||||
from unittest.mock import Mock
|
||||
|
||||
import websockets
|
||||
from pytest import FixtureRequest
|
||||
|
||||
from everest.testing.ocpp_utils.charge_point_utils import OcppTestConfiguration
|
||||
from ocpp.routing import create_route_map, on
|
||||
from ocpp.charge_point import ChargePoint
|
||||
|
||||
from everest.testing.ocpp_utils.charge_point_v16 import ChargePoint16
|
||||
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
|
||||
from everest.testing.ocpp_utils.charge_point_v21 import ChargePoint21
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.debug)
|
||||
|
||||
|
||||
class CentralSystem:
|
||||
"""Base central system used for tests to connect
|
||||
"""
|
||||
|
||||
def __init__(self, chargepoint_id, ocpp_version, port: Optional[int] = None):
|
||||
self.name = "CentralSystem"
|
||||
self.port = port
|
||||
self.chargepoint_id = chargepoint_id
|
||||
self.ocpp_version = ocpp_version
|
||||
|
||||
@abstractmethod
|
||||
async def on_connect(self, websocket):
|
||||
logging.error("'CentralSystem' did not implement 'on_connect'!")
|
||||
|
||||
@abstractmethod
|
||||
async def wait_for_chargepoint(self, timeout=30, wait_for_bootnotification=True):
|
||||
logging.error(
|
||||
"'CentralSystem' did not implement 'wait_for_chargepoint'!")
|
||||
return None
|
||||
|
||||
@abstractmethod
|
||||
async def start(self, ssl_context=None):
|
||||
logging.error("'CentralSystem' did not implement 'start'!")
|
||||
|
||||
|
||||
class LocalCentralSystem(CentralSystem):
|
||||
"""Wrapper for CSMS websocket server. Holds a reference to a single connected chargepoint
|
||||
"""
|
||||
|
||||
def __init__(self, chargepoint_id, ocpp_version, port: Optional[int] = None):
|
||||
super().__init__(chargepoint_id, ocpp_version, port)
|
||||
self.name = "LocalCentralSystem"
|
||||
self.ws_server = None
|
||||
self.chargepoint = None
|
||||
self.chargepoint_set_event = asyncio.Event()
|
||||
self.function_overrides = []
|
||||
self.skip_validation = []
|
||||
|
||||
async def on_connect(self, websocket):
|
||||
""" For every new charge point that connects, create a ChargePoint
|
||||
instance and start listening for messages.
|
||||
"""
|
||||
path = websocket.path
|
||||
chargepoint_id = path.strip('/')
|
||||
if chargepoint_id == self.chargepoint_id:
|
||||
logging.debug(f"Chargepoint {chargepoint_id} connected")
|
||||
try:
|
||||
requested_protocols = websocket.request_headers[
|
||||
'Sec-WebSocket-Protocol']
|
||||
except KeyError:
|
||||
logging.error(
|
||||
"Client hasn't requested any Subprotocol. Closing Connection"
|
||||
)
|
||||
return await websocket.close()
|
||||
if websocket.subprotocol:
|
||||
logging.debug("Protocols Matched: %s", websocket.subprotocol)
|
||||
else:
|
||||
# In the websockets lib if no subprotocols are supported by the
|
||||
# client and the server, it proceeds without a subprotocol,
|
||||
# so we have to manually close the connection.
|
||||
logging.warning('Protocols Mismatched | Expected Subprotocols: %s,'
|
||||
' but client supports %s | Closing connection',
|
||||
websocket.available_subprotocols,
|
||||
requested_protocols)
|
||||
return await websocket.close()
|
||||
|
||||
if self.ocpp_version == 'ocpp1.6':
|
||||
cp = ChargePoint16(chargepoint_id, websocket)
|
||||
elif self.ocpp_version == 'ocpp2.0.1':
|
||||
cp = ChargePoint201(chargepoint_id, websocket)
|
||||
else:
|
||||
cp = ChargePoint21(chargepoint_id, websocket)
|
||||
self.chargepoint = cp
|
||||
self.chargepoint.pipe = True
|
||||
for override in self.function_overrides:
|
||||
setattr(self.chargepoint, override[0], override[1])
|
||||
self.chargepoint.route_map = create_route_map(self.chargepoint)
|
||||
for action in self.skip_validation:
|
||||
self.chargepoint.route_map[action]["_skip_schema_validation"] = True
|
||||
|
||||
self.chargepoint_set_event.set()
|
||||
await self.chargepoint.start()
|
||||
else:
|
||||
logging.warning(
|
||||
f"Connection on invalid path {chargepoint_id} received. Check the configuration of the ChargePointId.")
|
||||
return await websocket.close()
|
||||
|
||||
async def wait_for_chargepoint(self, timeout=30, wait_for_bootnotification=True) -> Union[ChargePoint16, ChargePoint201, ChargePoint21]:
|
||||
"""Waits for the chargepoint to connect to the CSMS
|
||||
|
||||
Args:
|
||||
timeout (int, optional): time in seconds until timeout occurs. Defaults to 30.
|
||||
wait_for_bootnotification (bool, optional): Indiciates if this method should wait until the chargepoint sends a BootNotification. Defaults to True.
|
||||
|
||||
Returns:
|
||||
ChargePoint: reference to ChargePoint16, ChargePoint201 or ChargePoint21
|
||||
"""
|
||||
try:
|
||||
logging.debug("Waiting for chargepoint to connect")
|
||||
await asyncio.wait_for(self.chargepoint_set_event.wait(), timeout)
|
||||
logging.debug("Chargepoint connected!")
|
||||
self.chargepoint_set_event.clear()
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
raise asyncio.exceptions.TimeoutError(
|
||||
"Timeout while waiting for the chargepoint to connect.")
|
||||
|
||||
if wait_for_bootnotification:
|
||||
t_timeout = time.time() + timeout
|
||||
received_boot_notification = False
|
||||
while (time.time() < t_timeout and not received_boot_notification):
|
||||
raw_message = await asyncio.wait_for(self.chargepoint.wait_for_message(), timeout=timeout)
|
||||
# FIXME(piet): Make proper check for BootNotification
|
||||
received_boot_notification = "BootNotification" in raw_message
|
||||
|
||||
if not received_boot_notification:
|
||||
raise asyncio.exceptions.TimeoutError(
|
||||
"Timeout while waiting for BootNotification.")
|
||||
|
||||
await asyncio.sleep(1)
|
||||
return self.chargepoint
|
||||
|
||||
@asynccontextmanager
|
||||
async def start(self, ssl_context=None):
|
||||
"""Starts the websocket server
|
||||
"""
|
||||
self.ws_server = await websockets.serve(
|
||||
self.on_connect,
|
||||
'0.0.0.0',
|
||||
self.port,
|
||||
subprotocols=[self.ocpp_version.value],
|
||||
ssl=ssl_context
|
||||
)
|
||||
if self.port is None:
|
||||
self.port = self.ws_server.sockets[0].getsockname()[1]
|
||||
logging.info(f"Server port was not set, setting to {self.port}")
|
||||
logging.debug(f"Server Started listening to new {self.ocpp_version} connections.")
|
||||
|
||||
yield
|
||||
|
||||
self.ws_server.close()
|
||||
await self.ws_server.wait_closed()
|
||||
|
||||
|
||||
def inject_csms_v21_mock(cs: CentralSystem) -> Mock:
|
||||
""" Given a not yet started CentralSystem, add mock overrides for _any_ action handler.
|
||||
|
||||
If not touched, those will simply proxy any request.
|
||||
|
||||
However, they allow later change of the CSMS return values:
|
||||
|
||||
Example:
|
||||
|
||||
@inject_csms_mock
|
||||
async def test_foo(central_system_v201: CentralSystem):
|
||||
central_system_v21.mock.on_get_15118_ev_certificate.side_effect = [
|
||||
call_result21.Get15118EVCertificatePayload(status=response_status,
|
||||
exi_response=exi_response)]
|
||||
"""
|
||||
|
||||
def catch_mock(mock, method_name, method):
|
||||
method_mock = getattr(mock, method_name)
|
||||
|
||||
@on(method._on_action)
|
||||
@wraps(method)
|
||||
def _method(*args, **kwargs):
|
||||
mock_res = method_mock(*args, **kwargs)
|
||||
if method_mock.side_effect:
|
||||
return mock_res
|
||||
return method(cs.chargepoint, *args, **kwargs)
|
||||
|
||||
return _method
|
||||
|
||||
mock = Mock(spec=ChargePoint21)
|
||||
charge_point_action_handlers = {
|
||||
k: v for k, v in ChargePoint21.__dict__.items() if hasattr(v, "_on_action")}
|
||||
for action_name, action_method in charge_point_action_handlers.items():
|
||||
cs.function_overrides.append(
|
||||
(action_name, catch_mock(mock, action_name, action_method)))
|
||||
return mock
|
||||
|
||||
|
||||
def inject_csms_v201_mock(cs: CentralSystem) -> Mock:
|
||||
""" Given a not yet started CentralSystem, add mock overrides for _any_ action handler.
|
||||
|
||||
If not touched, those will simply proxy any request.
|
||||
|
||||
However, they allow later change of the CSMS return values:
|
||||
|
||||
Example:
|
||||
|
||||
@inject_csms_mock
|
||||
async def test_foo(central_system_v201: CentralSystem):
|
||||
central_system_v201.mock.on_get_15118_ev_certificate.side_effect = [
|
||||
call_result201.Get15118EVCertificatePayload(status=response_status,
|
||||
exi_response=exi_response)]
|
||||
"""
|
||||
|
||||
def catch_mock(mock, method_name, method):
|
||||
method_mock = getattr(mock, method_name)
|
||||
|
||||
@on(method._on_action)
|
||||
@wraps(method)
|
||||
def _method(*args, **kwargs):
|
||||
mock_res = method_mock(*args, **kwargs)
|
||||
if method_mock.side_effect:
|
||||
return mock_res
|
||||
return method(cs.chargepoint, *args, **kwargs)
|
||||
|
||||
return _method
|
||||
|
||||
mock = Mock(spec=ChargePoint201)
|
||||
charge_point_action_handlers = {
|
||||
k: v for k, v in ChargePoint201.__dict__.items() if hasattr(v, "_on_action")}
|
||||
for action_name, action_method in charge_point_action_handlers.items():
|
||||
cs.function_overrides.append(
|
||||
(action_name, catch_mock(mock, action_name, action_method)))
|
||||
return mock
|
||||
|
||||
|
||||
def inject_csms_v16_mock(cs: CentralSystem) -> Mock:
|
||||
""" Given a not yet started CentralSystem, add mock overrides for _any_ action handler.
|
||||
|
||||
If not touched, those will simply proxy any request.
|
||||
|
||||
However, they allow later change of the CSMS return values:
|
||||
|
||||
Example:
|
||||
|
||||
@inject_csms_mock
|
||||
async def test_foo(central_system_v201: CentralSystem):
|
||||
central_system_v201.mock.on_get_15118_ev_certificate.side_effect = [
|
||||
call_result201.Get15118EVCertificatePayload(status=response_status,
|
||||
exi_response=exi_response)]
|
||||
"""
|
||||
|
||||
def catch_mock(mock, method_name, method):
|
||||
method_mock = getattr(mock, method_name)
|
||||
|
||||
@on(method._on_action)
|
||||
@wraps(method)
|
||||
def _method(*args, **kwargs):
|
||||
mock_res = method_mock(*args, **kwargs)
|
||||
if method_mock.side_effect:
|
||||
return mock_res
|
||||
return method(cs.chargepoint, *args, **kwargs)
|
||||
|
||||
return _method
|
||||
|
||||
mock = Mock(spec=ChargePoint16)
|
||||
charge_point_action_handlers = {
|
||||
k: v for k, v in ChargePoint16.__dict__.items() if hasattr(v, "_on_action")}
|
||||
for action_name, action_method in charge_point_action_handlers.items():
|
||||
cs.function_overrides.append(
|
||||
(action_name, catch_mock(mock, action_name, action_method)))
|
||||
return mock
|
||||
|
||||
|
||||
def determine_ssl_context(request: FixtureRequest, test_config: OcppTestConfiguration) -> ssl.SSLContext | None:
|
||||
""" Determine CSMS SSL Context: Default take from test_config, can be overwritten by csms_tls marker """
|
||||
|
||||
csms_tls_enabled = test_config.csms_tls_enabled
|
||||
if test_config.certificate_info:
|
||||
csms_tls_cert = test_config.certificate_info.csms_cert
|
||||
csms_tls_key = test_config.certificate_info.csms_key
|
||||
csms_tls_passphrase = test_config.certificate_info.csms_passphrase
|
||||
csms_tls_root_ca = test_config.certificate_info.csms_root_ca
|
||||
else:
|
||||
csms_tls_cert = None
|
||||
csms_tls_key = None
|
||||
csms_tls_passphrase = None
|
||||
csms_tls_root_ca = None
|
||||
csms_tls_verify_client_certificate = test_config.csms_tls_verify_client_certificate
|
||||
|
||||
if csms_tls_marker := request.node.get_closest_marker("csms_tls"):
|
||||
if csms_tls_marker.args:
|
||||
csms_tls_enabled = csms_tls_marker.args[0]
|
||||
else:
|
||||
# provided marker always enabled tls if not explicitly set to False
|
||||
csms_tls_enabled = True
|
||||
marker_kwargs = csms_tls_marker.kwargs
|
||||
if "certificate" in marker_kwargs:
|
||||
csms_tls_cert = marker_kwargs["certificate"]
|
||||
if "private_key" in marker_kwargs:
|
||||
csms_tls_key = marker_kwargs["private_key"]
|
||||
if "passphrase" in marker_kwargs:
|
||||
csms_tls_passphrase = marker_kwargs["passphrase"]
|
||||
if "root_ca" in marker_kwargs:
|
||||
csms_tls_root_ca = marker_kwargs["root_ca"]
|
||||
if "verify_client_certificate" in marker_kwargs:
|
||||
csms_tls_verify_client_certificate = marker_kwargs["verify_client_certificate"]
|
||||
|
||||
if csms_tls_enabled:
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(csms_tls_cert,
|
||||
csms_tls_key,
|
||||
csms_tls_passphrase)
|
||||
if csms_tls_verify_client_certificate:
|
||||
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
||||
ssl_context.load_verify_locations(csms_tls_root_ca)
|
||||
return ssl_context
|
||||
else:
|
||||
return None
|
||||
@@ -0,0 +1,300 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import OpenSSL.crypto as crypto
|
||||
import logging
|
||||
import time
|
||||
import asyncio
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Any, Union
|
||||
from typing import Optional
|
||||
|
||||
from ocpp.messages import unpack
|
||||
from ocpp.charge_point import ChargePoint as CP
|
||||
from ocpp.charge_point import snake_to_camel_case, camel_to_snake_case, asdict, remove_nones
|
||||
|
||||
|
||||
@dataclass
|
||||
class ChargePointInfo:
|
||||
charge_point_id: str = "cp001"
|
||||
charge_point_vendor: Optional[str] = None
|
||||
charge_point_model: Optional[str] = None
|
||||
firmware_version: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthorizationInfo:
|
||||
emaid: str
|
||||
valid_id_tag_1: str
|
||||
valid_id_tag_2: str
|
||||
invalid_id_tag: str
|
||||
parent_id_tag: str
|
||||
invalid_parent_id_tag: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CertificateInfo:
|
||||
csms_root_ca: Path
|
||||
csms_root_ca_key: Path
|
||||
csms_root_ca_invalid: Path
|
||||
csms_cert: Path
|
||||
csms_key: Path
|
||||
csms_passphrase: str
|
||||
mf_root_ca: Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class FirmwareInfo:
|
||||
update_file: Path
|
||||
update_file_signature: Path
|
||||
|
||||
|
||||
@dataclass
|
||||
class OcppTestConfiguration:
|
||||
csms_tls_enabled: bool = False
|
||||
csms_tls_verify_client_certificate: bool = False
|
||||
csms_port: str = 9000
|
||||
csms_host: str = "127.0.0.1"
|
||||
charge_point_info: ChargePointInfo = field(default_factory=ChargePointInfo)
|
||||
config_path: Optional[Path] = None
|
||||
authorization_info: Optional[AuthorizationInfo] = None
|
||||
certificate_info: Optional[CertificateInfo] = None
|
||||
firmware_info: Optional[FirmwareInfo] = None
|
||||
|
||||
|
||||
class ValidationMode(str, Enum):
|
||||
STRICT = "STRICT"
|
||||
EASY = "EASY"
|
||||
|
||||
|
||||
class TestUtility:
|
||||
__test__ = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.messages = []
|
||||
self.validation_mode = ValidationMode.EASY
|
||||
self.forbidden_actions = []
|
||||
|
||||
|
||||
async def wait_for_and_validate(meta_data: TestUtility, charge_point: CP, exp_action: str,
|
||||
exp_payload, validate_payload_func=None, timeout: int = 30) -> Union[bool, Any]:
|
||||
|
||||
"""This method waits for an expected message specified by the message_type, the action and the payload to be received.
|
||||
It also considers the meta_data that contains the message history, the validation mode and forbidden actions.
|
||||
|
||||
Args:
|
||||
meta_data (TestUtility): contains the message history, the validation mode and forbidden actions that are considered in the validation
|
||||
charge_point (CP): The instance of the wrapper around the chargepoint websocket connection
|
||||
exp_action (str): The expected OCPP action (e.g. StatusNotification, BootNotification, etc.)
|
||||
exp_payload (_type_): The expected payload. Can be of type dict or can also be a call or call_result of the ocpp lib. If a dict is given,
|
||||
only the subset of the entries given in the dict will be validated
|
||||
validate_payload_func (function, optional): Optional validation function that can be used for more complex validations. Defaults to None.
|
||||
timeout (int, optional): time in seconds until waiting for the exp_payload times out. Defaults to 30.
|
||||
|
||||
Returns:
|
||||
Union[bool, Any]: True if valid message found, response if applicable, else False.
|
||||
"""
|
||||
|
||||
logging.debug(f"Waiting for {exp_action}")
|
||||
|
||||
# check if expected message has been sent already
|
||||
if (exp_message_has_already_been_sent(meta_data, exp_action, exp_payload, validate_payload_func)):
|
||||
return True
|
||||
|
||||
response = await validate_incoming_messages(
|
||||
meta_data, charge_point, exp_action, exp_payload, validate_payload_func, timeout, False
|
||||
)
|
||||
|
||||
if response:
|
||||
return response
|
||||
|
||||
logging.info("This is the message history")
|
||||
charge_point.message_history.log_history()
|
||||
return False
|
||||
|
||||
|
||||
async def wait_for_and_validate_next_message_only_with_specific_action(meta_data: TestUtility, charge_point: CP, exp_action: str,
|
||||
exp_payload, validate_payload_func=None, timeout: int = 30) -> Union[bool, Any]:
|
||||
|
||||
"""This method waits for an expected message specified by the message_type, the action and the payload to be received.
|
||||
It also considers the meta_data that contains the message history, the validation mode and forbidden actions.
|
||||
It will only check the first message with the expected action.
|
||||
|
||||
Args:
|
||||
meta_data (TestUtility): contains the message history, the validation mode and forbidden actions that are considered in the validation
|
||||
charge_point (CP): The instance of the wrapper around the chargepoint websocket connection
|
||||
exp_action (str): The expected OCPP action (e.g. StatusNotification, BootNotification, etc.)
|
||||
exp_payload (_type_): The expected payload. Can be of type dict or can also be a call or call_result of the ocpp lib. If a dict is given,
|
||||
only the subset of the entries given in the dict will be validated
|
||||
validate_payload_func (function, optional): Optional validation function that can be used for more complex validations. Defaults to None.
|
||||
timeout (int, optional): time in seconds until waiting for the exp_payload times out. Defaults to 30.
|
||||
|
||||
Returns:
|
||||
Union[bool, Any]: True if valid message found, response if applicable, else False.
|
||||
"""
|
||||
|
||||
logging.debug(f"Waiting for {exp_action}")
|
||||
|
||||
# check if expected message has been sent already
|
||||
if (exp_message_has_already_been_sent(meta_data, exp_action, exp_payload, validate_payload_func)):
|
||||
return True
|
||||
|
||||
response = await validate_incoming_messages(
|
||||
meta_data, charge_point, exp_action, exp_payload, validate_payload_func, timeout, False
|
||||
)
|
||||
|
||||
if response:
|
||||
return response
|
||||
|
||||
logging.info("This is the message history")
|
||||
charge_point.message_history.log_history()
|
||||
return False
|
||||
|
||||
|
||||
def exp_message_has_already_been_sent(meta_data: TestUtility, exp_action: str, exp_payload, validate_payload_func=None):
|
||||
if (meta_data.validation_mode == ValidationMode.EASY and
|
||||
validate_against_old_messages(meta_data,
|
||||
exp_action, exp_payload, validate_payload_func)):
|
||||
logging.debug(
|
||||
f"Found correct message {exp_action} with payload {exp_payload} in old messages")
|
||||
logging.debug("OK!")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def validate_incoming_messages(meta_data: TestUtility, charge_point: CP, exp_action: str, exp_payload, validate_payload_func=None, timeout: int = 30, check_next_only=False):
|
||||
t_timeout = time.time() + timeout
|
||||
while (time.time() < t_timeout):
|
||||
try:
|
||||
raw_message = await asyncio.wait_for(charge_point.wait_for_message(), timeout=timeout)
|
||||
charge_point.message_event.clear()
|
||||
msg = unpack(raw_message)
|
||||
if (msg.message_type_id == 4):
|
||||
logging.debug("Received CallError")
|
||||
elif (msg.action != None):
|
||||
logging.debug(f"Received Call {msg.action}")
|
||||
elif (msg.message_type_id == 3):
|
||||
logging.debug("Received CallResult")
|
||||
|
||||
meta_data.messages.append(msg)
|
||||
|
||||
response = validate_message(
|
||||
msg, exp_action, exp_payload, validate_payload_func, meta_data)
|
||||
if response != False:
|
||||
logging.debug("Message validated successfully!")
|
||||
meta_data.messages.remove(msg)
|
||||
if response:
|
||||
return response
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
if (msg.message_type_id != 4):
|
||||
logging.debug(
|
||||
f"This message {msg.action} with payload {msg.payload} was not what I waited for")
|
||||
logging.debug(f"I wait for {exp_payload}")
|
||||
# add msg to messages and wait for next message
|
||||
meta_data.messages.append(msg)
|
||||
if (check_next_only and msg.message_type_id == 2 and msg.action == exp_action):
|
||||
return False
|
||||
except asyncio.TimeoutError:
|
||||
logging.debug("Timeout while waiting for new message")
|
||||
logging.info(
|
||||
f"Timeout while waiting for correct message with action {exp_action} and payload {exp_payload}")
|
||||
return False
|
||||
|
||||
|
||||
def validate_against_old_messages(meta_data, exp_action, exp_payload, validate_payload_func=None):
|
||||
if meta_data.messages:
|
||||
for msg in meta_data.messages:
|
||||
response = validate_message(
|
||||
msg, exp_action, exp_payload, validate_payload_func, meta_data)
|
||||
if response:
|
||||
meta_data.messages.remove(msg)
|
||||
return response
|
||||
return False
|
||||
|
||||
|
||||
def contains_expected_response(expected: dict, msg_payload: dict):
|
||||
for k, v in expected.items():
|
||||
if k not in msg_payload:
|
||||
return False
|
||||
|
||||
if isinstance(v, dict):
|
||||
if not isinstance(msg_payload[k], dict):
|
||||
return False
|
||||
if not contains_expected_response(v, msg_payload[k]):
|
||||
return False
|
||||
elif msg_payload[k] != v:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validate_message(msg, exp_action, exp_payload, validate_payload_func, meta_data):
|
||||
|
||||
if (msg.message_type_id == 4):
|
||||
return False
|
||||
|
||||
if msg.action in meta_data.forbidden_actions:
|
||||
logging.error(
|
||||
f"Forbidden action {msg.action} was sent by the charge point")
|
||||
assert False
|
||||
|
||||
try:
|
||||
if ((msg.message_type_id == 2 and msg.action == exp_action) or msg.message_type_id == 3):
|
||||
if (validate_payload_func == None):
|
||||
if not isinstance(exp_payload, dict):
|
||||
exp_payload = asdict(exp_payload)
|
||||
exp_payload = remove_nones(snake_to_camel_case(exp_payload))
|
||||
if contains_expected_response(exp_payload, msg.payload):
|
||||
return camel_to_snake_case(msg.payload)
|
||||
elif meta_data.validation_mode == ValidationMode.STRICT and \
|
||||
msg.message_type_id != 3:
|
||||
assert False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return validate_payload_func(meta_data, msg, exp_payload)
|
||||
|
||||
else:
|
||||
return False
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
|
||||
class HistoryMessage:
|
||||
def __init__(self, message, initiator) -> None:
|
||||
self.message = message
|
||||
self.initiator = initiator
|
||||
self.time = datetime.now()
|
||||
|
||||
|
||||
class MessageHistory:
|
||||
def __init__(self) -> None:
|
||||
self.messages = []
|
||||
|
||||
def add_received(self, message):
|
||||
self.messages.append(HistoryMessage(message, "Chargepoint"))
|
||||
|
||||
def add_send(self, message):
|
||||
self.messages.append(HistoryMessage(message, "CSMS"))
|
||||
|
||||
def log_history(self):
|
||||
for message in self.messages:
|
||||
time = message.time.strftime("%d-%m-%Y, %H:%M:%S")
|
||||
logging.info(f"{time} {message.initiator}: {message.message}")
|
||||
|
||||
|
||||
def create_cert(serial_no, not_before, not_after, ca_cert, csr, ca_private_key):
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(serial_no)
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(not_after)
|
||||
cert.set_issuer(ca_cert.get_subject())
|
||||
cert.set_subject(csr.get_subject())
|
||||
cert.set_pubkey(csr.get_pubkey())
|
||||
cert.sign(ca_private_key, 'SHA256')
|
||||
|
||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
@@ -0,0 +1,358 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import OpenSSL.crypto as crypto
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
|
||||
|
||||
from ocpp.messages import unpack
|
||||
from ocpp.charge_point import camel_to_snake_case, snake_to_camel_case, asdict, remove_nones
|
||||
from ocpp.v16.datatypes import (
|
||||
IdTagInfo,
|
||||
)
|
||||
from ocpp.v16 import call, call_result
|
||||
from ocpp.v16.enums import (
|
||||
Action,
|
||||
RegistrationStatus,
|
||||
AuthorizationStatus,
|
||||
GenericStatus,
|
||||
DataTransferStatus
|
||||
)
|
||||
from ocpp.v16 import ChargePoint as cp
|
||||
from ocpp.routing import on
|
||||
|
||||
# for OCPP1.6 PnC whitepaper:
|
||||
from ocpp.v201 import call_result as call_result201
|
||||
from ocpp.v201.datatypes import IdTokenInfoType
|
||||
from ocpp.v201.enums import (
|
||||
AuthorizationStatusEnumType, GenericStatusEnumType, GetCertificateStatusEnumType)
|
||||
|
||||
from everest.testing.ocpp_utils.charge_point_utils import MessageHistory, create_cert
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class ChargePoint16(cp):
|
||||
|
||||
"""Wrapper for the OCPP1.6 chargepoint websocket client. Implementes the communication
|
||||
of messages sent from CSMS to chargepoint.
|
||||
"""
|
||||
|
||||
def __init__(self, cp_id, connection, response_timeout=30):
|
||||
super().__init__(cp_id, connection, response_timeout)
|
||||
self.pipeline = []
|
||||
self.pipe = False
|
||||
self.csr = None
|
||||
self.message_event = asyncio.Event()
|
||||
self.message_history = MessageHistory()
|
||||
|
||||
async def start(self):
|
||||
"""Start to receive, store and route incoming messages.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
message = await self._connection.recv()
|
||||
logging.debug(f"Chargepoint: \n{message}")
|
||||
self.message_history.add_received(message)
|
||||
|
||||
if (self.pipe):
|
||||
self.pipeline.append(message)
|
||||
self.message_event.set()
|
||||
|
||||
await self.route_message(message)
|
||||
self.message_event.clear()
|
||||
except ConnectionClosedOK:
|
||||
logging.debug("ConnectionClosedOK: Websocket is going down")
|
||||
except ConnectionClosedError:
|
||||
logging.debug("ConnectionClosedError: Websocket is going down")
|
||||
|
||||
async def stop(self):
|
||||
"""Drops the websocket connection
|
||||
"""
|
||||
await self._connection.close()
|
||||
|
||||
async def _send(self, message):
|
||||
"""Saves the given message to the MessageHistory and sends the message over the ws connection
|
||||
|
||||
Args:
|
||||
message (str): message
|
||||
"""
|
||||
logging.debug(f"CSMS: \n{message}")
|
||||
self.message_history.add_send(message)
|
||||
await self._connection.send(message)
|
||||
|
||||
async def wait_for_message(self):
|
||||
"""If no message is in the pipeline, this method waits for the next message.
|
||||
If there is one or more messages in the pipeline, it pops the latest message.
|
||||
"""
|
||||
if not self.pipeline:
|
||||
await self.message_event.wait()
|
||||
return self.pipeline.pop(0)
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification(
|
||||
self, charge_point_vendor: str, charge_point_model: str, **kwargs
|
||||
):
|
||||
logging.debug("Received a BootNotification")
|
||||
# connecting to mqtt server
|
||||
return call_result.BootNotification(
|
||||
current_time=datetime.now(timezone.utc).isoformat(),
|
||||
interval=1440,
|
||||
status=RegistrationStatus.accepted,
|
||||
)
|
||||
|
||||
@on(Action.heartbeat)
|
||||
def on_heartbeat(self, **kwargs):
|
||||
return call_result.Heartbeat(current_time=datetime.now(timezone.utc).isoformat())
|
||||
|
||||
@on(Action.authorize)
|
||||
def on_authorize(self, **kwargs):
|
||||
id_tag_info = IdTagInfo(status=AuthorizationStatus.accepted)
|
||||
return call_result.Authorize(id_tag_info=id_tag_info)
|
||||
|
||||
@on(Action.meter_values)
|
||||
def on_meter_values(self, **kwargs):
|
||||
return call_result.MeterValues()
|
||||
|
||||
@on(Action.status_notification)
|
||||
def on_status_notification(self, **kwargs):
|
||||
return call_result.StatusNotification()
|
||||
|
||||
@on(Action.start_transaction)
|
||||
def on_start_transaction(self, **kwargs):
|
||||
id_tag_info = IdTagInfo(status=AuthorizationStatus.accepted)
|
||||
return call_result.StartTransaction(transaction_id=1, id_tag_info=id_tag_info)
|
||||
|
||||
@on(Action.stop_transaction)
|
||||
def on_stop_transaction(self, **kwargs):
|
||||
return call_result.StopTransaction()
|
||||
|
||||
@on(Action.diagnostics_status_notification)
|
||||
def on_diagnostics_status_notification(self, **kwargs):
|
||||
return call_result.DiagnosticsStatusNotification()
|
||||
|
||||
@on(Action.sign_certificate)
|
||||
def on_sign_certificate(self, **kwargs):
|
||||
self.csr = kwargs['csr']
|
||||
return call_result.SignCertificate(GenericStatus.accepted)
|
||||
|
||||
@on(Action.security_event_notification)
|
||||
def on_security_event_notification(self, **kwargs):
|
||||
return call_result.SecurityEventNotification()
|
||||
|
||||
@on(Action.signed_firmware_status_notification)
|
||||
def on_signed_update_firmware_status_notificaion(self, **kwargs):
|
||||
return call_result.SignedFirmwareStatusNotification()
|
||||
|
||||
@on(Action.log_status_notification)
|
||||
def on_log_status_notification(self, **kwargs):
|
||||
return call_result.LogStatusNotification()
|
||||
|
||||
@on(Action.firmware_status_notification)
|
||||
def on_firmware_status_notification(self, **kwargs):
|
||||
return call_result.FirmwareStatusNotification()
|
||||
|
||||
@on(Action.data_transfer)
|
||||
def on_data_transfer(self, **kwargs):
|
||||
req = call.DataTransfer(**kwargs)
|
||||
if req.vendor_id == 'org.openchargealliance.iso15118pnc':
|
||||
if (req.message_id == "Authorize"):
|
||||
response = call_result201.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted
|
||||
)
|
||||
)
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.accepted,
|
||||
data=json.dumps(remove_nones(
|
||||
snake_to_camel_case(asdict(response))))
|
||||
)
|
||||
# Should not be part of DataTransfer.req from CP->CSMS
|
||||
elif (req.message_id == "CertificateSigned"):
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
# Should not be part of DataTransfer.req from CP->CSMS
|
||||
elif req.message_id == "DeleteCertificate":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
elif req.message_id == "Get15118EVCertificate":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
elif req.message_id == "GetCertificateStatus":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.accepted,
|
||||
data=json.dumps(remove_nones(snake_to_camel_case(asdict(
|
||||
call_result201.GetCertificateStatus(
|
||||
status=GetCertificateStatusEnumType.accepted,
|
||||
ocsp_result="anwfdiefnwenfinfinef"
|
||||
)
|
||||
))))
|
||||
)
|
||||
# Should not be part of DataTransfer.req from CP->CSMS
|
||||
elif req.message_id == "InstallCertificate":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
elif req.message_id == "SignCertificate":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.accepted,
|
||||
data=json.dumps(asdict(
|
||||
call_result201.SignCertificate(
|
||||
status=GenericStatusEnumType.accepted
|
||||
)
|
||||
))
|
||||
)
|
||||
# Should not be part of DataTransfer.req from CP->CSMS
|
||||
elif req.message_id == "TriggerMessage":
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
else:
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_message_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
else:
|
||||
return call_result.DataTransfer(
|
||||
status=DataTransferStatus.unknown_vendor_id,
|
||||
data="Please implement me"
|
||||
)
|
||||
|
||||
async def get_configuration_req(self, **kwargs):
|
||||
payload = call.GetConfiguration(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def change_configuration_req(self, **kwargs):
|
||||
payload = call.ChangeConfiguration(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_cache_req(self, **kwargs):
|
||||
payload = call.ClearCache()
|
||||
return await self.call(payload)
|
||||
|
||||
async def remote_start_transaction_req(self, **kwargs):
|
||||
payload = call.RemoteStartTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def remote_stop_transaction_req(self, **kwargs):
|
||||
payload = call.RemoteStopTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def unlock_connector_req(self, **kwargs):
|
||||
payload = call.UnlockConnector(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def change_availability_req(self, **kwargs):
|
||||
payload = call.ChangeAvailability(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reset_req(self, **kwargs):
|
||||
payload = call.Reset(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_local_list_version_req(self, **kwargs):
|
||||
payload = call.GetLocalListVersion()
|
||||
return await self.call(payload)
|
||||
|
||||
async def send_local_list_req(self, **kwargs):
|
||||
payload = call.SendLocalList(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reserve_now_req(self, **kwargs):
|
||||
payload = call.ReserveNow(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def cancel_reservation_req(self, **kwargs):
|
||||
payload = call.CancelReservation(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def trigger_message_req(self, **kwargs):
|
||||
payload = call.TriggerMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_charging_profile_req(self, payload: call.SetChargingProfile):
|
||||
logging.info(payload)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_composite_schedule(self, payload: call.GetCompositeSchedule) -> call_result.GetCompositeSchedule:
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_composite_schedule_req(self, **kwargs) -> call_result.GetCompositeSchedule:
|
||||
payload = call.GetCompositeSchedule(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_charging_profile_req(self, **kwargs):
|
||||
payload = call.ClearChargingProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def data_transfer_req(self, **kwargs):
|
||||
payload = call.DataTransfer(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def extended_trigger_message_req(self, **kwargs):
|
||||
payload = call.ExtendedTriggerMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def certificate_signed_req(self, **kwargs):
|
||||
if 'certificate_chain' not in kwargs:
|
||||
serial_no = 1
|
||||
not_before = -86400
|
||||
not_after = 86400*365
|
||||
|
||||
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(
|
||||
kwargs['csms_root_ca']).read())
|
||||
csr = crypto.load_certificate_request(
|
||||
crypto.FILETYPE_PEM, self.csr)
|
||||
ca_private_key = crypto.load_privatekey(
|
||||
crypto.FILETYPE_PEM, open(kwargs['csms_root_ca_key']).read(), str.encode('ocatool'))
|
||||
|
||||
cert = create_cert(serial_no, not_before,
|
||||
not_after, ca_cert, csr, ca_private_key)
|
||||
|
||||
payload = call.CertificateSigned(
|
||||
certificate_chain=cert.decode())
|
||||
return await self.call(payload)
|
||||
else:
|
||||
payload = call.CertificateSigned(
|
||||
certificate_chain=kwargs['certificate_chain'])
|
||||
return await self.call(payload)
|
||||
|
||||
async def install_certificate_req(self, **kwargs):
|
||||
payload = call.InstallCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_installed_certificate_ids_req(self, **kwargs):
|
||||
payload = call.GetInstalledCertificateIds(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def delete_certificate_req(self, **kwargs):
|
||||
payload = call.DeleteCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_log_req(self, **kwargs):
|
||||
payload = call.GetLog(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def signed_update_firmware_req(self, **kwargs):
|
||||
payload = call.SignedUpdateFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_diagnostics_req(self, **kwargs):
|
||||
payload = call.GetDiagnostics(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def update_firmware_req(self, **kwargs):
|
||||
payload = call.UpdateFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
@@ -0,0 +1,361 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ocpp.routing import on
|
||||
from ocpp.v201 import ChargePoint as cp
|
||||
from ocpp.v201 import call, call_result
|
||||
from ocpp.v201.datatypes import IdTokenInfoType, SetVariableDataType, GetVariableDataType, ComponentType, VariableType
|
||||
from ocpp.v201.enums import (
|
||||
Action,
|
||||
RegistrationStatusEnumType,
|
||||
AuthorizationStatusEnumType,
|
||||
AttributeEnumType,
|
||||
NotifyEVChargingNeedsStatusEnumType,
|
||||
GenericStatusEnumType
|
||||
)
|
||||
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
|
||||
|
||||
from everest.testing.ocpp_utils.charge_point_utils import MessageHistory
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class ChargePoint201(cp):
|
||||
"""Wrapper for the OCPP2.0.1 chargepoint websocket client. Implementes the communication
|
||||
of messages sent from CSMS to chargepoint.
|
||||
"""
|
||||
|
||||
def __init__(self, cp_id, connection, response_timeout=30):
|
||||
super().__init__(cp_id, connection, response_timeout)
|
||||
self.pipeline = []
|
||||
self.pipe = False
|
||||
self.csr = None
|
||||
self.message_event = asyncio.Event()
|
||||
self.message_history = MessageHistory()
|
||||
|
||||
async def start(self):
|
||||
"""Start to receive, store and route incoming messages.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
message = await self._connection.recv()
|
||||
logging.debug(f"Chargepoint: \n{message}")
|
||||
self.message_history.add_received(message)
|
||||
|
||||
if (self.pipe):
|
||||
self.pipeline.append(message)
|
||||
self.message_event.set()
|
||||
|
||||
await self.route_message(message)
|
||||
self.message_event.clear()
|
||||
except ConnectionClosedOK:
|
||||
logging.debug("ConnectionClosedOK: Websocket is going down")
|
||||
except ConnectionClosedError:
|
||||
logging.debug("ConnectionClosedError: Websocket is going down")
|
||||
|
||||
async def stop(self):
|
||||
await self._connection.close()
|
||||
|
||||
async def _send(self, message):
|
||||
logging.debug(f"CSMS: \n{message}")
|
||||
self.message_history.add_send(message)
|
||||
await self._connection.send(message)
|
||||
|
||||
async def wait_for_message(self):
|
||||
"""If no message is in the pipeline, this method waits for the next message.
|
||||
If there is one or more messages in the pipeline, it pops the latest message.
|
||||
"""
|
||||
if not self.pipeline:
|
||||
await self.message_event.wait()
|
||||
return self.pipeline.pop(0)
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification(self, **kwargs):
|
||||
logging.debug("Received a BootNotification")
|
||||
return call_result.BootNotification(current_time=datetime.now().isoformat(),
|
||||
interval=300, status=RegistrationStatusEnumType.accepted)
|
||||
|
||||
@on(Action.status_notification)
|
||||
def on_status_notification(self, **kwargs):
|
||||
return call_result.StatusNotification()
|
||||
|
||||
@on(Action.heartbeat)
|
||||
def on_heartbeat(self, **kwargs):
|
||||
return call_result.Heartbeat(current_time=datetime.now(timezone.utc).isoformat())
|
||||
|
||||
@on(Action.authorize)
|
||||
def on_authorize(self, **kwargs):
|
||||
return call_result.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted
|
||||
)
|
||||
)
|
||||
|
||||
@on(Action.notify_report)
|
||||
def on_notify_report(self, **kwargs):
|
||||
return call_result.NotifyReport()
|
||||
|
||||
@on(Action.log_status_notification)
|
||||
def on_log_status_notification(self, **kwargs):
|
||||
return call_result.LogStatusNotification()
|
||||
|
||||
@on(Action.firmware_status_notification)
|
||||
def on_firmware_status_notification(self, **kwargs):
|
||||
return call_result.FirmwareStatusNotification()
|
||||
|
||||
@on(Action.transaction_event)
|
||||
def on_transaction_event(self, **kwargs):
|
||||
return call_result.TransactionEvent()
|
||||
|
||||
@on(Action.meter_values)
|
||||
def on_meter_values(self, **kwargs):
|
||||
return call_result.MeterValues()
|
||||
|
||||
@on(Action.notify_charging_limit)
|
||||
def on_notify_charging_limit(self, **kwargs):
|
||||
return call_result.NotifyChargingLimit()
|
||||
|
||||
@on(Action.notify_customer_information)
|
||||
def on_notify_customer_information(self, **kwargs):
|
||||
return call_result.NotifyCustomerInformation()
|
||||
|
||||
@on(Action.notify_ev_charging_needs)
|
||||
def on_notify_ev_charging_needs(self, **kwargs):
|
||||
return call_result.NotifyEVChargingNeeds(status=NotifyEVChargingNeedsStatusEnumType.accepted)
|
||||
|
||||
@on(Action.notify_ev_charging_schedule)
|
||||
def on_notify_ev_charging_schedule(self, **kwargs):
|
||||
return call_result.NotifyEVChargingSchedule(status=GenericStatusEnumType.accepted)
|
||||
|
||||
@on(Action.notify_event)
|
||||
def on_notify_event(self, **kwargs):
|
||||
return call_result.NotifyEvent()
|
||||
|
||||
@on(Action.notify_monitoring_report)
|
||||
def on_notify_monitoring_report(self, **kwargs):
|
||||
return call_result.NotifyMonitoringReport()
|
||||
|
||||
@on(Action.publish_firmware_status_notification)
|
||||
def on_publish_firmware_status_notification(self, **kwargs):
|
||||
return call_result.PublishFirmwareStatusNotification()
|
||||
|
||||
@on(Action.report_charging_profiles)
|
||||
def on_report_charging_profiles(self, **kwargs):
|
||||
return call_result.ReportChargingProfiles()
|
||||
|
||||
@on(Action.reservation_status_update)
|
||||
def on_reservation_status_update(self, **kwargs):
|
||||
return call_result.ReservationStatusUpdate()
|
||||
|
||||
@on(Action.security_event_notification)
|
||||
def on_security_event_notification(self, **kwargs):
|
||||
return call_result.SecurityEventNotification()
|
||||
|
||||
@on(Action.sign_certificate)
|
||||
def on_sign_certificate(self, **kwargs):
|
||||
return call_result.SignCertificate(status=GenericStatusEnumType.accepted)
|
||||
|
||||
@on(Action.get_15118_ev_certificate)
|
||||
def on_get_15118_ev_certificate(self, **kwargs):
|
||||
return call_result.Get15118EVCertificate(status=GenericStatusEnumType.accepted,
|
||||
exi_response="")
|
||||
|
||||
@on(Action.get_certificate_status)
|
||||
def on_get_certificate_status(self, **kwargs):
|
||||
return call_result.GetCertificateStatus(status=GenericStatusEnumType.accepted,
|
||||
ocsp_result="")
|
||||
|
||||
@on(Action.data_transfer)
|
||||
def on_data_transfer(self, **kwargs):
|
||||
return call_result.DataTransfer(status=GenericStatusEnumType.accepted, data="")
|
||||
|
||||
async def set_variables_req(self, **kwargs):
|
||||
payload = call.SetVariables(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_config_variables_req(self, component_name, variable_name, value):
|
||||
el = SetVariableDataType(
|
||||
attribute_value=value,
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
component=ComponentType(
|
||||
name=component_name
|
||||
),
|
||||
variable=VariableType(
|
||||
name=variable_name
|
||||
)
|
||||
)
|
||||
payload = call.SetVariables([el])
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_variables_req(self, **kwargs):
|
||||
payload = call.GetVariables(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_config_variables_req(self, component_name, variable_name):
|
||||
el = GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name=component_name
|
||||
),
|
||||
variable=VariableType(
|
||||
name=variable_name
|
||||
),
|
||||
attribute_type=AttributeEnumType.actual
|
||||
)
|
||||
payload = call.GetVariables([el])
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_base_report_req(self, **kwargs):
|
||||
payload = call.GetBaseReport(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_report_req(self, **kwargs):
|
||||
payload = call.GetReport(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reset_req(self, **kwargs):
|
||||
payload = call.Reset(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def request_start_transaction_req(self, **kwargs):
|
||||
payload = call.RequestStartTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def request_stop_transaction_req(self, **kwargs):
|
||||
payload = call.RequestStopTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def change_availablility_req(self, **kwargs):
|
||||
payload = call.ChangeAvailability(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_cache_req(self, **kwargs):
|
||||
payload = call.ClearCache(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def cancel_reservation_req(self, **kwargs):
|
||||
payload = call.CancelReservation(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def certificate_signed_req(self, **kwargs):
|
||||
payload = call.CertificateSigned(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_charging_profile_req(self, **kwargs):
|
||||
payload = call.ClearChargingProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_display_message_req(self, **kwargs):
|
||||
payload = call.ClearDisplayMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_charging_limit_req(self, **kwargs):
|
||||
payload = call.ClearedChargingLimit(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_variable_monitoring_req(self, **kwargs):
|
||||
payload = call.ClearVariableMonitoringd(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def cost_update_req(self, **kwargs):
|
||||
payload = call.CostUpdated(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def customer_information_req(self, **kwargs):
|
||||
payload = call.CustomerInformation(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def data_transfer_req(self, **kwargs):
|
||||
payload = call.DataTransfer(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def delete_certificate_req(self, **kwargs):
|
||||
payload = call.DeleteCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_charging_profiles_req(self, **kwargs):
|
||||
payload = call.GetChargingProfiles(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_composite_schedule_req(self, **kwargs):
|
||||
payload = call.GetCompositeSchedule(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_display_nessages_req(self, **kwargs):
|
||||
payload = call.GetDisplayMessages(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_installed_certificate_ids_req(self, **kwargs):
|
||||
payload = call.GetInstalledCertificateIds(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_local_list_version(self, **kwargs):
|
||||
payload = call.GetLocalListVersion(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_log_req(self, **kwargs):
|
||||
payload = call.GetLog(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_transaction_status_req(self, **kwargs):
|
||||
payload = call.GetTransactionStatus(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def install_certificate_req(self, **kwargs):
|
||||
payload = call.InstallCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def publish_firmware_req(self, **kwargs):
|
||||
payload = call.PublishFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reserve_now_req(self, **kwargs):
|
||||
payload = call.ReserveNow(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def send_local_list_req(self, **kwargs):
|
||||
payload = call.SendLocalList(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_charging_profile_req(self, **kwargs):
|
||||
payload = call.SetChargingProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_display_message_req(self, **kwargs):
|
||||
payload = call.SetDisplayMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_monitoring_base_req(self, **kwargs):
|
||||
payload = call.SetMonitoringBase(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_monitoring_level_req(self, **kwargs):
|
||||
payload = call.SetMonitoringLevel(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_network_profile_req(self, **kwargs):
|
||||
payload = call.SetNetworkProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_variable_monitoring_req(self, **kwargs):
|
||||
payload = call.SetVariableMonitoring(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def trigger_message_req(self, **kwargs):
|
||||
payload = call.TriggerMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def unlock_connector_req(self, **kwargs):
|
||||
payload = call.UnlockConnector(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def unpublish_firmware_req(self, **kwargs):
|
||||
payload = call.UnpublishFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def update_firmware(self, **kwargs):
|
||||
payload = call.UpdateFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
@@ -0,0 +1,393 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ocpp.routing import on
|
||||
from ocpp.v21 import ChargePoint as cp
|
||||
from ocpp.v21 import call, call_result
|
||||
from ocpp.v21.datatypes import IdTokenInfoType, SetVariableDataType, GetVariableDataType, ComponentType, VariableType
|
||||
from ocpp.v21.enums import (
|
||||
Action,
|
||||
RegistrationStatusEnumType,
|
||||
AuthorizationStatusEnumType,
|
||||
AttributeEnumType,
|
||||
NotifyEVChargingNeedsStatusEnumType,
|
||||
GenericStatusEnumType
|
||||
)
|
||||
from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError
|
||||
|
||||
from everest.testing.ocpp_utils.charge_point_utils import MessageHistory
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
class ChargePoint21(cp):
|
||||
"""Wrapper for the OCPP2.1 chargepoint websocket client. Implementes the communication
|
||||
of messages sent from CSMS to chargepoint.
|
||||
"""
|
||||
|
||||
def __init__(self, cp_id, connection, response_timeout=30):
|
||||
super().__init__(cp_id, connection, response_timeout)
|
||||
self.pipeline = []
|
||||
self.pipe = False
|
||||
self.csr = None
|
||||
self.message_event = asyncio.Event()
|
||||
self.message_history = MessageHistory()
|
||||
|
||||
async def start(self):
|
||||
"""Start to receive, store and route incoming messages.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
message = await self._connection.recv()
|
||||
logging.debug(f"Chargepoint: \n{message}")
|
||||
self.message_history.add_received(message)
|
||||
|
||||
if (self.pipe):
|
||||
self.pipeline.append(message)
|
||||
self.message_event.set()
|
||||
|
||||
await self.route_message(message)
|
||||
self.message_event.clear()
|
||||
except ConnectionClosedOK:
|
||||
logging.debug("ConnectionClosedOK: Websocket is going down")
|
||||
except ConnectionClosedError:
|
||||
logging.debug("ConnectionClosedError: Websocket is going down")
|
||||
|
||||
async def stop(self):
|
||||
await self._connection.close()
|
||||
|
||||
async def _send(self, message):
|
||||
logging.debug(f"CSMS: \n{message}")
|
||||
self.message_history.add_send(message)
|
||||
await self._connection.send(message)
|
||||
|
||||
async def wait_for_message(self):
|
||||
"""If no message is in the pipeline, this method waits for the next message.
|
||||
If there is one or more messages in the pipeline, it pops the latest message.
|
||||
"""
|
||||
if not self.pipeline:
|
||||
await self.message_event.wait()
|
||||
return self.pipeline.pop(0)
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification(self, **kwargs):
|
||||
logging.debug("Received a BootNotification")
|
||||
return call_result.BootNotification(current_time=datetime.now().isoformat(),
|
||||
interval=300, status=RegistrationStatusEnumType.accepted)
|
||||
|
||||
@on(Action.status_notification)
|
||||
def on_status_notification(self, **kwargs):
|
||||
return call_result.StatusNotification()
|
||||
|
||||
@on(Action.heartbeat)
|
||||
def on_heartbeat(self, **kwargs):
|
||||
return call_result.Heartbeat(current_time=datetime.now(timezone.utc).isoformat())
|
||||
|
||||
@on(Action.authorize)
|
||||
def on_authorize(self, **kwargs):
|
||||
return call_result.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted
|
||||
)
|
||||
)
|
||||
|
||||
@on(Action.notify_report)
|
||||
def on_notify_report(self, **kwargs):
|
||||
return call_result.NotifyReport()
|
||||
|
||||
@on(Action.log_status_notification)
|
||||
def on_log_status_notification(self, **kwargs):
|
||||
return call_result.LogStatusNotification()
|
||||
|
||||
@on(Action.firmware_status_notification)
|
||||
def on_firmware_status_notification(self, **kwargs):
|
||||
return call_result.FirmwareStatusNotification()
|
||||
|
||||
@on(Action.transaction_event)
|
||||
def on_transaction_event(self, **kwargs):
|
||||
return call_result.TransactionEvent()
|
||||
|
||||
@on(Action.meter_values)
|
||||
def on_meter_values(self, **kwargs):
|
||||
return call_result.MeterValues()
|
||||
|
||||
@on(Action.notify_charging_limit)
|
||||
def on_notify_charging_limit(self, **kwargs):
|
||||
return call_result.NotifyChargingLimit()
|
||||
|
||||
@on(Action.notify_customer_information)
|
||||
def on_notify_customer_information(self, **kwargs):
|
||||
return call_result.NotifyCustomerInformation()
|
||||
|
||||
@on(Action.notify_ev_charging_needs)
|
||||
def on_notify_ev_charging_needs(self, **kwargs):
|
||||
return call_result.NotifyEVChargingNeeds(status=NotifyEVChargingNeedsStatusEnumType.accepted)
|
||||
|
||||
@on(Action.notify_ev_charging_schedule)
|
||||
def on_notify_ev_charging_schedule(self, **kwargs):
|
||||
return call_result.NotifyEVChargingSchedule(status=GenericStatusEnumType.accepted)
|
||||
|
||||
@on(Action.notify_event)
|
||||
def on_notify_event(self, **kwargs):
|
||||
return call_result.NotifyEvent()
|
||||
|
||||
@on(Action.notify_monitoring_report)
|
||||
def on_notify_monitoring_report(self, **kwargs):
|
||||
return call_result.NotifyMonitoringReport()
|
||||
|
||||
@on(Action.publish_firmware_status_notification)
|
||||
def on_publish_firmware_status_notification(self, **kwargs):
|
||||
return call_result.PublishFirmwareStatusNotification()
|
||||
|
||||
@on(Action.report_charging_profiles)
|
||||
def on_report_charging_profiles(self, **kwargs):
|
||||
return call_result.ReportChargingProfiles()
|
||||
|
||||
@on(Action.reservation_status_update)
|
||||
def on_reservation_status_update(self, **kwargs):
|
||||
return call_result.ReservationStatusUpdate()
|
||||
|
||||
@on(Action.security_event_notification)
|
||||
def on_security_event_notification(self, **kwargs):
|
||||
return call_result.SecurityEventNotification()
|
||||
|
||||
@on(Action.sign_certificate)
|
||||
def on_sign_certificate(self, **kwargs):
|
||||
return call_result.SignCertificate(status=GenericStatusEnumType.accepted)
|
||||
|
||||
@on(Action.get15118_ev_certificate)
|
||||
def on_get_15118_ev_certificate(self, **kwargs):
|
||||
return call_result.Get15118EVCertificate(status=GenericStatusEnumType.accepted,
|
||||
exi_response="")
|
||||
|
||||
@on(Action.get_certificate_status)
|
||||
def on_get_certificate_status(self, **kwargs):
|
||||
return call_result.GetCertificateStatus(status=GenericStatusEnumType.accepted,
|
||||
ocsp_result="")
|
||||
|
||||
@on(Action.data_transfer)
|
||||
def on_data_transfer(self, **kwargs):
|
||||
return call_result.DataTransfer(status=GenericStatusEnumType.accepted, data="")
|
||||
|
||||
async def set_variables_req(self, **kwargs):
|
||||
payload = call.SetVariables(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_config_variables_req(self, component_name, variable_name, value):
|
||||
el = SetVariableDataType(
|
||||
attribute_value=value,
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
component=ComponentType(
|
||||
name=component_name
|
||||
),
|
||||
variable=VariableType(
|
||||
name=variable_name
|
||||
)
|
||||
)
|
||||
payload = call.SetVariables([el])
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_variables_req(self, **kwargs):
|
||||
payload = call.GetVariables(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_config_variables_req(self, component_name, variable_name):
|
||||
el = GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name=component_name
|
||||
),
|
||||
variable=VariableType(
|
||||
name=variable_name
|
||||
),
|
||||
attribute_type=AttributeEnumType.actual
|
||||
)
|
||||
payload = call.GetVariables([el])
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_base_report_req(self, **kwargs):
|
||||
payload = call.GetBaseReport(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_report_req(self, **kwargs):
|
||||
payload = call.GetReport(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reset_req(self, **kwargs):
|
||||
payload = call.Reset(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def request_start_transaction_req(self, **kwargs):
|
||||
payload = call.RequestStartTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def request_stop_transaction_req(self, **kwargs):
|
||||
payload = call.RequestStopTransaction(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def change_availablility_req(self, **kwargs):
|
||||
payload = call.ChangeAvailability(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_cache_req(self, **kwargs):
|
||||
payload = call.ClearCache(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def cancel_reservation_req(self, **kwargs):
|
||||
payload = call.CancelReservation(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def certificate_signed_req(self, **kwargs):
|
||||
payload = call.CertificateSigned(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_charging_profile_req(self, **kwargs):
|
||||
payload = call.ClearChargingProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_display_message_req(self, **kwargs):
|
||||
payload = call.ClearDisplayMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_charging_limit_req(self, **kwargs):
|
||||
payload = call.ClearedChargingLimit(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_variable_monitoring_req(self, **kwargs):
|
||||
payload = call.ClearVariableMonitoringd(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def cost_update_req(self, **kwargs):
|
||||
payload = call.CostUpdated(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def customer_information_req(self, **kwargs):
|
||||
payload = call.CustomerInformation(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def data_transfer_req(self, **kwargs):
|
||||
payload = call.DataTransfer(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def delete_certificate_req(self, **kwargs):
|
||||
payload = call.DeleteCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_charging_profiles_req(self, **kwargs):
|
||||
payload = call.GetChargingProfiles(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_composite_schedule_req(self, **kwargs):
|
||||
payload = call.GetCompositeSchedule(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_display_nessages_req(self, **kwargs):
|
||||
payload = call.GetDisplayMessages(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_installed_certificate_ids_req(self, **kwargs):
|
||||
payload = call.GetInstalledCertificateIds(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_local_list_version(self, **kwargs):
|
||||
payload = call.GetLocalListVersion(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_log_req(self, **kwargs):
|
||||
payload = call.GetLog(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_transaction_status_req(self, **kwargs):
|
||||
payload = call.GetTransactionStatus(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def install_certificate_req(self, **kwargs):
|
||||
payload = call.InstallCertificate(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def publish_firmware_req(self, **kwargs):
|
||||
payload = call.PublishFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def reserve_now_req(self, **kwargs):
|
||||
payload = call.ReserveNow(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def send_local_list_req(self, **kwargs):
|
||||
payload = call.SendLocalList(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_charging_profile_req(self, **kwargs):
|
||||
payload = call.SetChargingProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_display_message_req(self, **kwargs):
|
||||
payload = call.SetDisplayMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_monitoring_base_req(self, **kwargs):
|
||||
payload = call.SetMonitoringBase(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_monitoring_level_req(self, **kwargs):
|
||||
payload = call.SetMonitoringLevel(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_network_profile_req(self, **kwargs):
|
||||
payload = call.SetNetworkProfile(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def set_variable_monitoring_req(self, **kwargs):
|
||||
payload = call.SetVariableMonitoring(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def trigger_message_req(self, **kwargs):
|
||||
payload = call.TriggerMessage(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def unlock_connector_req(self, **kwargs):
|
||||
payload = call.UnlockConnector(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def unpublish_firmware_req(self, **kwargs):
|
||||
payload = call.UnpublishFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def update_firmware(self, **kwargs):
|
||||
payload = call.UpdateFirmware(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def notify_allowed_energy_transfer_request(self, **kwargs):
|
||||
payload = call.NotifyAllowedEnergyTransfer(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
# --- OCPP 2.1 R04: DER Control (CSMS -> CS) ---
|
||||
|
||||
async def set_der_control_req(self, **kwargs):
|
||||
payload = call.SetDERControl(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def get_der_control_req(self, **kwargs):
|
||||
payload = call.GetDERControl(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
async def clear_der_control_req(self, **kwargs):
|
||||
payload = call.ClearDERControl(**kwargs)
|
||||
return await self.call(payload)
|
||||
|
||||
# --- OCPP 2.1 R04: DER Control (CS -> CSMS) response handlers ---
|
||||
|
||||
@on(Action.notify_der_start_stop)
|
||||
def on_notify_der_start_stop(self, **kwargs):
|
||||
return call_result.NotifyDERStartStop()
|
||||
|
||||
@on(Action.notify_der_alarm)
|
||||
def on_notify_der_alarm(self, **kwargs):
|
||||
return call_result.NotifyDERAlarm()
|
||||
|
||||
@on(Action.report_der_control)
|
||||
def on_report_der_control(self, **kwargs):
|
||||
return call_result.ReportDERControl()
|
||||
@@ -0,0 +1,227 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from pyftpdlib import servers
|
||||
from pyftpdlib.authorizers import DummyAuthorizer
|
||||
from pyftpdlib.handlers import FTPHandler
|
||||
|
||||
from everest.testing.core_utils.common import OCPPVersion
|
||||
from everest.testing.core_utils._configuration.everest_environment_setup import EverestEnvironmentOCPPConfiguration
|
||||
from everest.testing.core_utils.controller.everest_test_controller import EverestTestController
|
||||
from everest.testing.ocpp_utils.central_system import CentralSystem, LocalCentralSystem, inject_csms_v201_mock, inject_csms_v16_mock, \
|
||||
determine_ssl_context, inject_csms_v21_mock
|
||||
from everest.testing.ocpp_utils.charge_point_utils import TestUtility, OcppTestConfiguration
|
||||
|
||||
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".")))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ocpp_version(request) -> OCPPVersion:
|
||||
ocpp_version = request.node.get_closest_marker("ocpp_version")
|
||||
if ocpp_version:
|
||||
return OCPPVersion(request.node.get_closest_marker("ocpp_version").args[0])
|
||||
else:
|
||||
return OCPPVersion("ocpp1.6")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ocpp_config(request, central_system: CentralSystem, test_config: OcppTestConfiguration, ocpp_version: OCPPVersion):
|
||||
ocpp_config_marker = request.node.get_closest_marker("ocpp_config")
|
||||
|
||||
ocpp_configuration_strategies_marker = request.node.get_closest_marker(
|
||||
"ocpp_config_adaptions")
|
||||
ocpp_configuration_strategies = []
|
||||
if ocpp_configuration_strategies_marker:
|
||||
for v in ocpp_configuration_strategies_marker.args:
|
||||
assert hasattr(v,
|
||||
"adjust_ocpp_configuration"), "Arguments to 'ocpp_config_adaptions' must all provide interface of OCPPConfigAdjustmentStrategy"
|
||||
ocpp_configuration_strategies.append(v)
|
||||
|
||||
return EverestEnvironmentOCPPConfiguration(
|
||||
central_system_port=central_system.port,
|
||||
central_system_host="127.0.0.1",
|
||||
ocpp_version=ocpp_version,
|
||||
template_ocpp_config=Path(
|
||||
ocpp_config_marker.args[0]) if ocpp_config_marker else None,
|
||||
device_model_component_config_path=Path(f"{request.config.getoption('--everest-prefix')}/share/everest/modules/OCPP201/component_config"),
|
||||
configuration_strategies=ocpp_configuration_strategies
|
||||
)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def central_system(request, ocpp_version: OCPPVersion, test_config):
|
||||
"""Fixture for CentralSystem. Can be started as TLS or
|
||||
plain websocket depending on the request parameter.
|
||||
"""
|
||||
|
||||
ssl_context = determine_ssl_context(request, test_config)
|
||||
|
||||
central_system_marker = request.node.get_closest_marker(
|
||||
'custom_central_system')
|
||||
|
||||
if central_system_marker:
|
||||
assert isinstance(central_system_marker.args[0], CentralSystem)
|
||||
cs = central_system_marker.args[0]
|
||||
else:
|
||||
cs = LocalCentralSystem(test_config.charge_point_info.charge_point_id,
|
||||
ocpp_version=ocpp_version)
|
||||
|
||||
if request.node.get_closest_marker('inject_csms_mock'):
|
||||
if ocpp_version == OCPPVersion.ocpp201:
|
||||
mock = inject_csms_v201_mock(cs)
|
||||
elif ocpp_version == OCPPVersion.ocpp16:
|
||||
mock = inject_csms_v16_mock(cs)
|
||||
else:
|
||||
mock = inject_csms_v21_mock(cs)
|
||||
cs.mock = mock
|
||||
|
||||
async with cs.start(ssl_context):
|
||||
yield cs
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def charge_point(central_system: CentralSystem, test_controller: EverestTestController):
|
||||
"""Fixture for ChargePoint16. Requires central_system_v201 and test_controller. Starts test_controller immediately
|
||||
"""
|
||||
test_controller.start()
|
||||
cp = await central_system.wait_for_chargepoint()
|
||||
yield cp
|
||||
await cp.stop()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_utility():
|
||||
"""Fixture for test case meta data
|
||||
"""
|
||||
return TestUtility()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_config():
|
||||
return OcppTestConfiguration()
|
||||
|
||||
|
||||
class FtpThread(Thread):
|
||||
def __init__(self, directory, port, test_config: OcppTestConfiguration, ftp_socket,
|
||||
group=None, target=None, name=None, args=..., kwargs=None, *, daemon=None):
|
||||
super().__init__(group, target, name, args, kwargs, daemon=daemon)
|
||||
self.directory = directory
|
||||
self.port = port
|
||||
self.test_config = test_config
|
||||
self.ftp_socket = ftp_socket
|
||||
|
||||
def set_directory(self, directory):
|
||||
self.directory = directory
|
||||
|
||||
def set_port(self, port):
|
||||
self.port = port
|
||||
|
||||
def set_test_config(self, test_config: OcppTestConfiguration):
|
||||
self.test_config = test_config
|
||||
|
||||
def set_socket(self, ftp_socket):
|
||||
self.ftp_socket = ftp_socket
|
||||
|
||||
def stop(self):
|
||||
self.server.close_all()
|
||||
|
||||
def run(self):
|
||||
shutil.copyfile(self.test_config.firmware_info.update_file, os.path.join(
|
||||
self.directory, "firmware_update.pnx"))
|
||||
shutil.copyfile(self.test_config.firmware_info.update_file_signature,
|
||||
os.path.join(self.directory, "firmware_update.pnx.base64"))
|
||||
|
||||
authorizer = DummyAuthorizer()
|
||||
authorizer.add_user(getpass.getuser(), "12345",
|
||||
self.directory, perm="elradfmwMT")
|
||||
|
||||
handler = FTPHandler
|
||||
handler.authorizer = authorizer
|
||||
|
||||
self.server = servers.FTPServer(self.ftp_socket, handler)
|
||||
|
||||
self.server.serve_forever()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ftp_server(test_config: OcppTestConfiguration):
|
||||
"""This fixture creates a temporary directory and starts
|
||||
a local ftp server connected to that directory. The temporary
|
||||
directory is deleted afterwards
|
||||
"""
|
||||
|
||||
d = tempfile.mkdtemp(prefix='tmp_ftp')
|
||||
address = ("127.0.0.1", 0)
|
||||
ftp_socket = socket.socket()
|
||||
ftp_socket.bind(address)
|
||||
port = ftp_socket.getsockname()[1]
|
||||
|
||||
ftp_thread = FtpThread(directory=d, port=port,
|
||||
test_config=test_config, ftp_socket=ftp_socket)
|
||||
ftp_thread.daemon = True
|
||||
ftp_thread.start()
|
||||
|
||||
yield ftp_thread
|
||||
|
||||
ftp_thread.stop()
|
||||
|
||||
shutil.rmtree(d)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def central_system_v16(central_system):
|
||||
""" Note: This is only for backwards compatibility; use central_system directly! """
|
||||
yield central_system
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def central_system_v201(central_system):
|
||||
""" Note: This is only for backwards compatibility; use central_system directly! """
|
||||
yield central_system
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def central_system_v21(central_system):
|
||||
""" Note: This is only for backwards compatibility; use central_system directly! """
|
||||
yield central_system
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def charge_point_v16(charge_point):
|
||||
""" Note: This is only for backwards compatibility; use charge_point directly! """
|
||||
yield charge_point
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def charge_point_v201(charge_point):
|
||||
""" Note: This is only for backwards compatibility; use charge_point directly! """
|
||||
yield charge_point
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def charge_point_v21(charge_point):
|
||||
""" Note: This is only for backwards compatibility; use charge_point directly! """
|
||||
yield charge_point
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def central_system_v16_standalone(request, central_system: CentralSystem, test_controller: EverestTestController):
|
||||
""" Note: This is only for backwards compatibility; use central_system + test_controller directly!
|
||||
|
||||
Fixture for standalone central system. Requires central_system_v16 and test_controller. Starts test_controller immediately
|
||||
"""
|
||||
test_controller.start()
|
||||
yield central_system
|
||||
test_controller.stop()
|
||||
717
tools/EVerest-main/applications/utils/requirements-bazel.txt
Normal file
717
tools/EVerest-main/applications/utils/requirements-bazel.txt
Normal file
@@ -0,0 +1,717 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile --python-version 3.10 --generate-hashes --override /tmp/overrides.txt --output-file=applications/utils/requirements-bazel.txt applications/dependency_manager/setup.cfg applications/utils/ev-dev-tools/setup.cfg applications/utils/everest-testing/setup.cfg lib/everest/framework/everestpy/setup.cfg
|
||||
attrs==26.1.0 \
|
||||
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
|
||||
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
backports-asyncio-runner==1.2.0 \
|
||||
--hash=sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5 \
|
||||
--hash=sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162
|
||||
# via pytest-asyncio
|
||||
certifi==2026.2.25 \
|
||||
--hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \
|
||||
--hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7
|
||||
# via requests
|
||||
cffi==1.15.1 \
|
||||
--hash=sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5 \
|
||||
--hash=sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef \
|
||||
--hash=sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104 \
|
||||
--hash=sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426 \
|
||||
--hash=sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405 \
|
||||
--hash=sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375 \
|
||||
--hash=sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a \
|
||||
--hash=sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e \
|
||||
--hash=sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc \
|
||||
--hash=sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf \
|
||||
--hash=sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185 \
|
||||
--hash=sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497 \
|
||||
--hash=sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3 \
|
||||
--hash=sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35 \
|
||||
--hash=sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c \
|
||||
--hash=sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83 \
|
||||
--hash=sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21 \
|
||||
--hash=sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca \
|
||||
--hash=sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984 \
|
||||
--hash=sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac \
|
||||
--hash=sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd \
|
||||
--hash=sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee \
|
||||
--hash=sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a \
|
||||
--hash=sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2 \
|
||||
--hash=sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192 \
|
||||
--hash=sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7 \
|
||||
--hash=sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585 \
|
||||
--hash=sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f \
|
||||
--hash=sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e \
|
||||
--hash=sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27 \
|
||||
--hash=sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b \
|
||||
--hash=sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e \
|
||||
--hash=sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e \
|
||||
--hash=sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d \
|
||||
--hash=sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c \
|
||||
--hash=sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415 \
|
||||
--hash=sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82 \
|
||||
--hash=sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02 \
|
||||
--hash=sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314 \
|
||||
--hash=sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325 \
|
||||
--hash=sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c \
|
||||
--hash=sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3 \
|
||||
--hash=sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914 \
|
||||
--hash=sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045 \
|
||||
--hash=sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d \
|
||||
--hash=sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9 \
|
||||
--hash=sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5 \
|
||||
--hash=sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2 \
|
||||
--hash=sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c \
|
||||
--hash=sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3 \
|
||||
--hash=sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2 \
|
||||
--hash=sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8 \
|
||||
--hash=sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d \
|
||||
--hash=sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d \
|
||||
--hash=sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9 \
|
||||
--hash=sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162 \
|
||||
--hash=sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76 \
|
||||
--hash=sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4 \
|
||||
--hash=sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e \
|
||||
--hash=sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9 \
|
||||
--hash=sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6 \
|
||||
--hash=sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b \
|
||||
--hash=sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01 \
|
||||
--hash=sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0
|
||||
# via cryptography
|
||||
charset-normalizer==3.4.6 \
|
||||
--hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \
|
||||
--hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \
|
||||
--hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \
|
||||
--hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \
|
||||
--hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \
|
||||
--hash=sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0 \
|
||||
--hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \
|
||||
--hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \
|
||||
--hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \
|
||||
--hash=sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8 \
|
||||
--hash=sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264 \
|
||||
--hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \
|
||||
--hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \
|
||||
--hash=sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4 \
|
||||
--hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \
|
||||
--hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \
|
||||
--hash=sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa \
|
||||
--hash=sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95 \
|
||||
--hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \
|
||||
--hash=sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297 \
|
||||
--hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \
|
||||
--hash=sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e \
|
||||
--hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \
|
||||
--hash=sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8 \
|
||||
--hash=sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0 \
|
||||
--hash=sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9 \
|
||||
--hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \
|
||||
--hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \
|
||||
--hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \
|
||||
--hash=sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565 \
|
||||
--hash=sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7 \
|
||||
--hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \
|
||||
--hash=sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b \
|
||||
--hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \
|
||||
--hash=sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687 \
|
||||
--hash=sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9 \
|
||||
--hash=sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14 \
|
||||
--hash=sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89 \
|
||||
--hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \
|
||||
--hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \
|
||||
--hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \
|
||||
--hash=sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a \
|
||||
--hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \
|
||||
--hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \
|
||||
--hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \
|
||||
--hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \
|
||||
--hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \
|
||||
--hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \
|
||||
--hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \
|
||||
--hash=sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532 \
|
||||
--hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \
|
||||
--hash=sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae \
|
||||
--hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \
|
||||
--hash=sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64 \
|
||||
--hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \
|
||||
--hash=sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557 \
|
||||
--hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \
|
||||
--hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \
|
||||
--hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \
|
||||
--hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \
|
||||
--hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \
|
||||
--hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \
|
||||
--hash=sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597 \
|
||||
--hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \
|
||||
--hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \
|
||||
--hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \
|
||||
--hash=sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54 \
|
||||
--hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \
|
||||
--hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \
|
||||
--hash=sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4 \
|
||||
--hash=sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7 \
|
||||
--hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \
|
||||
--hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \
|
||||
--hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \
|
||||
--hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \
|
||||
--hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \
|
||||
--hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \
|
||||
--hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \
|
||||
--hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \
|
||||
--hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \
|
||||
--hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \
|
||||
--hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \
|
||||
--hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \
|
||||
--hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \
|
||||
--hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \
|
||||
--hash=sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6 \
|
||||
--hash=sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc \
|
||||
--hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \
|
||||
--hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \
|
||||
--hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \
|
||||
--hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \
|
||||
--hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \
|
||||
--hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \
|
||||
--hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \
|
||||
--hash=sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237 \
|
||||
--hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \
|
||||
--hash=sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778 \
|
||||
--hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \
|
||||
--hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \
|
||||
--hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \
|
||||
--hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \
|
||||
--hash=sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f \
|
||||
--hash=sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5 \
|
||||
--hash=sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611 \
|
||||
--hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \
|
||||
--hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \
|
||||
--hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \
|
||||
--hash=sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b \
|
||||
--hash=sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db \
|
||||
--hash=sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e \
|
||||
--hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \
|
||||
--hash=sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd \
|
||||
--hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \
|
||||
--hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \
|
||||
--hash=sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8 \
|
||||
--hash=sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe \
|
||||
--hash=sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058 \
|
||||
--hash=sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17 \
|
||||
--hash=sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833 \
|
||||
--hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \
|
||||
--hash=sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550 \
|
||||
--hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \
|
||||
--hash=sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2 \
|
||||
--hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \
|
||||
--hash=sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982 \
|
||||
--hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \
|
||||
--hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \
|
||||
--hash=sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104 \
|
||||
--hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659
|
||||
# via requests
|
||||
cryptography==41.0.7 \
|
||||
--hash=sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960 \
|
||||
--hash=sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a \
|
||||
--hash=sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc \
|
||||
--hash=sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a \
|
||||
--hash=sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf \
|
||||
--hash=sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1 \
|
||||
--hash=sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39 \
|
||||
--hash=sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406 \
|
||||
--hash=sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a \
|
||||
--hash=sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a \
|
||||
--hash=sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c \
|
||||
--hash=sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be \
|
||||
--hash=sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15 \
|
||||
--hash=sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2 \
|
||||
--hash=sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d \
|
||||
--hash=sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157 \
|
||||
--hash=sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003 \
|
||||
--hash=sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248 \
|
||||
--hash=sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a \
|
||||
--hash=sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec \
|
||||
--hash=sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309 \
|
||||
--hash=sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7 \
|
||||
--hash=sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d
|
||||
# via
|
||||
# everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
# pyopenssl
|
||||
exceptiongroup==1.3.1 \
|
||||
--hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \
|
||||
--hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598
|
||||
# via pytest
|
||||
idna==3.11 \
|
||||
--hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \
|
||||
--hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902
|
||||
# via requests
|
||||
iniconfig==2.0.0 \
|
||||
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
|
||||
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
|
||||
# via pytest
|
||||
jinja2==3.1.2 \
|
||||
--hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
|
||||
--hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
|
||||
# via
|
||||
# edm-tool (applications/dependency_manager/setup.cfg)
|
||||
# ev-dev-tools (applications/utils/ev-dev-tools/setup.cfg)
|
||||
jsonschema==4.26.0 \
|
||||
--hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \
|
||||
--hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce
|
||||
# via
|
||||
# ev-dev-tools (applications/utils/ev-dev-tools/setup.cfg)
|
||||
# ocpp
|
||||
jsonschema-specifications==2023.11.1 \
|
||||
--hash=sha256:c9b234904ffe02f079bf91b14d79987faa685fd4b39c377a0996954c0090b9ca \
|
||||
--hash=sha256:f596778ab612b3fd29f72ea0d990393d0540a5aab18bf0407a46632eab540779
|
||||
# via jsonschema
|
||||
markupsafe==2.1.3 \
|
||||
--hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
|
||||
--hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
|
||||
--hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
|
||||
--hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
|
||||
--hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
|
||||
--hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
|
||||
--hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
|
||||
--hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
|
||||
--hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
|
||||
--hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
|
||||
--hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
|
||||
--hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
|
||||
--hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
|
||||
--hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
|
||||
--hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
|
||||
--hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
|
||||
--hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
|
||||
--hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
|
||||
--hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
|
||||
--hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
|
||||
--hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
|
||||
--hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
|
||||
--hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
|
||||
--hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
|
||||
--hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
|
||||
--hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
|
||||
--hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
|
||||
--hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
|
||||
--hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
|
||||
--hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
|
||||
--hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
|
||||
--hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
|
||||
--hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
|
||||
--hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
|
||||
--hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
|
||||
--hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
|
||||
--hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
|
||||
--hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
|
||||
--hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
|
||||
--hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
|
||||
--hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
|
||||
--hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
|
||||
--hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
|
||||
--hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
|
||||
--hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
|
||||
--hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
|
||||
--hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
|
||||
--hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
|
||||
--hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
|
||||
--hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
|
||||
--hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
|
||||
--hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
|
||||
--hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
|
||||
--hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
|
||||
--hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
|
||||
--hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
|
||||
--hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
|
||||
--hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
|
||||
--hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
|
||||
--hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
|
||||
# via jinja2
|
||||
ocpp==2.0.0 \
|
||||
--hash=sha256:45ea3f35cb0afd7a0acbc1cdf2cfd107caf371c24aca7e7a03491405bf39e626 \
|
||||
--hash=sha256:bbc203ae5edeb7baf43a9a24b73c6a7473179197437fb39c641f0d93afce5dc0
|
||||
# via
|
||||
# --override /tmp/overrides.txt
|
||||
# everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
packaging==23.0 \
|
||||
--hash=sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2 \
|
||||
--hash=sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97
|
||||
# via pytest
|
||||
paho-mqtt==2.1.0 \
|
||||
--hash=sha256:12d6e7511d4137555a3f6ea167ae846af2c7357b10bc6fa4f7c3968fc1723834 \
|
||||
--hash=sha256:6db9ba9b34ed5bc6b6e3812718c7e06e2fd7444540df2455d2c51bd58808feee
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via pytest
|
||||
pycparser==2.21 \
|
||||
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
|
||||
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
|
||||
# via cffi
|
||||
pyftpdlib==2.2.0 \
|
||||
--hash=sha256:4ba0642078792df63dd3b2e9c8f838f2a3ecf428c7518d5921c0530d53512acf
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via pytest
|
||||
pyopenssl==25.1.0 \
|
||||
--hash=sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab \
|
||||
--hash=sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
pytest==9.0.2 \
|
||||
--hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \
|
||||
--hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11
|
||||
# via
|
||||
# everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
# pytest-asyncio
|
||||
pytest-asyncio==1.3.0 \
|
||||
--hash=sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5 \
|
||||
--hash=sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
pyyaml==6.0.1 \
|
||||
--hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
|
||||
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
|
||||
--hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
|
||||
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
|
||||
--hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
|
||||
--hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
|
||||
--hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
|
||||
--hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
|
||||
--hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
|
||||
--hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
|
||||
--hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
|
||||
--hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
|
||||
--hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
|
||||
--hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
|
||||
--hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
|
||||
--hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
|
||||
--hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
|
||||
--hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
|
||||
--hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
|
||||
--hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
|
||||
--hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
|
||||
--hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
|
||||
--hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
|
||||
--hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
|
||||
--hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
|
||||
--hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
|
||||
--hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
|
||||
--hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
|
||||
--hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
|
||||
--hash=sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef \
|
||||
--hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
|
||||
--hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
|
||||
--hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
|
||||
--hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
|
||||
--hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
|
||||
--hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
|
||||
--hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
|
||||
--hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
|
||||
--hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
|
||||
--hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
|
||||
--hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
|
||||
--hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
|
||||
--hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
|
||||
--hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
|
||||
--hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
|
||||
--hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
|
||||
--hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
|
||||
--hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
|
||||
--hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
|
||||
--hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
|
||||
--hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
|
||||
# via
|
||||
# edm-tool (applications/dependency_manager/setup.cfg)
|
||||
# ev-dev-tools (applications/utils/ev-dev-tools/setup.cfg)
|
||||
# everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
referencing==0.31.0 \
|
||||
--hash=sha256:381b11e53dd93babb55696c71cf42aef2d36b8a150c49bf0bc301e36d536c882 \
|
||||
--hash=sha256:cc28f2c88fbe7b961a7817a0abc034c09a1e36358f82fedb4ffdf29a25398863
|
||||
# via
|
||||
# jsonschema
|
||||
# jsonschema-specifications
|
||||
requests==2.33.1 \
|
||||
--hash=sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517 \
|
||||
--hash=sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a
|
||||
# via edm-tool (applications/dependency_manager/setup.cfg)
|
||||
rpds-py==0.30.0 \
|
||||
--hash=sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f \
|
||||
--hash=sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136 \
|
||||
--hash=sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3 \
|
||||
--hash=sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7 \
|
||||
--hash=sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65 \
|
||||
--hash=sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4 \
|
||||
--hash=sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169 \
|
||||
--hash=sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf \
|
||||
--hash=sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4 \
|
||||
--hash=sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2 \
|
||||
--hash=sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c \
|
||||
--hash=sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4 \
|
||||
--hash=sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3 \
|
||||
--hash=sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6 \
|
||||
--hash=sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7 \
|
||||
--hash=sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89 \
|
||||
--hash=sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85 \
|
||||
--hash=sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6 \
|
||||
--hash=sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa \
|
||||
--hash=sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb \
|
||||
--hash=sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6 \
|
||||
--hash=sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87 \
|
||||
--hash=sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856 \
|
||||
--hash=sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4 \
|
||||
--hash=sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f \
|
||||
--hash=sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53 \
|
||||
--hash=sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229 \
|
||||
--hash=sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad \
|
||||
--hash=sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23 \
|
||||
--hash=sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db \
|
||||
--hash=sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038 \
|
||||
--hash=sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27 \
|
||||
--hash=sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00 \
|
||||
--hash=sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18 \
|
||||
--hash=sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083 \
|
||||
--hash=sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c \
|
||||
--hash=sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738 \
|
||||
--hash=sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898 \
|
||||
--hash=sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e \
|
||||
--hash=sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7 \
|
||||
--hash=sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08 \
|
||||
--hash=sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6 \
|
||||
--hash=sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551 \
|
||||
--hash=sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e \
|
||||
--hash=sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288 \
|
||||
--hash=sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df \
|
||||
--hash=sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0 \
|
||||
--hash=sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2 \
|
||||
--hash=sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05 \
|
||||
--hash=sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0 \
|
||||
--hash=sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464 \
|
||||
--hash=sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5 \
|
||||
--hash=sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404 \
|
||||
--hash=sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7 \
|
||||
--hash=sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139 \
|
||||
--hash=sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394 \
|
||||
--hash=sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb \
|
||||
--hash=sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15 \
|
||||
--hash=sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff \
|
||||
--hash=sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed \
|
||||
--hash=sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6 \
|
||||
--hash=sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e \
|
||||
--hash=sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95 \
|
||||
--hash=sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d \
|
||||
--hash=sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950 \
|
||||
--hash=sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3 \
|
||||
--hash=sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5 \
|
||||
--hash=sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97 \
|
||||
--hash=sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e \
|
||||
--hash=sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e \
|
||||
--hash=sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b \
|
||||
--hash=sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd \
|
||||
--hash=sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad \
|
||||
--hash=sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8 \
|
||||
--hash=sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425 \
|
||||
--hash=sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221 \
|
||||
--hash=sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d \
|
||||
--hash=sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825 \
|
||||
--hash=sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51 \
|
||||
--hash=sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e \
|
||||
--hash=sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f \
|
||||
--hash=sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8 \
|
||||
--hash=sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f \
|
||||
--hash=sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d \
|
||||
--hash=sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07 \
|
||||
--hash=sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877 \
|
||||
--hash=sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31 \
|
||||
--hash=sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58 \
|
||||
--hash=sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94 \
|
||||
--hash=sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28 \
|
||||
--hash=sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000 \
|
||||
--hash=sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1 \
|
||||
--hash=sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1 \
|
||||
--hash=sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7 \
|
||||
--hash=sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7 \
|
||||
--hash=sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40 \
|
||||
--hash=sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d \
|
||||
--hash=sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0 \
|
||||
--hash=sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84 \
|
||||
--hash=sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f \
|
||||
--hash=sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a \
|
||||
--hash=sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7 \
|
||||
--hash=sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419 \
|
||||
--hash=sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8 \
|
||||
--hash=sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a \
|
||||
--hash=sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9 \
|
||||
--hash=sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be \
|
||||
--hash=sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed \
|
||||
--hash=sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a \
|
||||
--hash=sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d \
|
||||
--hash=sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324 \
|
||||
--hash=sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f \
|
||||
--hash=sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2 \
|
||||
--hash=sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f \
|
||||
--hash=sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5
|
||||
# via
|
||||
# jsonschema
|
||||
# referencing
|
||||
six==1.16.0 \
|
||||
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
|
||||
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
|
||||
# via python-dateutil
|
||||
stringcase==1.2.0 \
|
||||
--hash=sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008
|
||||
# via ev-dev-tools (applications/utils/ev-dev-tools/setup.cfg)
|
||||
tomli==2.4.1 \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||
# via pytest
|
||||
typing-extensions==4.15.0 \
|
||||
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
|
||||
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
|
||||
# via
|
||||
# exceptiongroup
|
||||
# pyopenssl
|
||||
# pytest-asyncio
|
||||
urllib3==2.6.3 \
|
||||
--hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \
|
||||
--hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4
|
||||
# via requests
|
||||
websockets==13.1 \
|
||||
--hash=sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a \
|
||||
--hash=sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54 \
|
||||
--hash=sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23 \
|
||||
--hash=sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7 \
|
||||
--hash=sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135 \
|
||||
--hash=sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700 \
|
||||
--hash=sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf \
|
||||
--hash=sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5 \
|
||||
--hash=sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e \
|
||||
--hash=sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c \
|
||||
--hash=sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02 \
|
||||
--hash=sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a \
|
||||
--hash=sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418 \
|
||||
--hash=sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f \
|
||||
--hash=sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3 \
|
||||
--hash=sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68 \
|
||||
--hash=sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978 \
|
||||
--hash=sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20 \
|
||||
--hash=sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295 \
|
||||
--hash=sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b \
|
||||
--hash=sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6 \
|
||||
--hash=sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb \
|
||||
--hash=sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a \
|
||||
--hash=sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa \
|
||||
--hash=sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0 \
|
||||
--hash=sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a \
|
||||
--hash=sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238 \
|
||||
--hash=sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c \
|
||||
--hash=sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084 \
|
||||
--hash=sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19 \
|
||||
--hash=sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d \
|
||||
--hash=sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7 \
|
||||
--hash=sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9 \
|
||||
--hash=sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79 \
|
||||
--hash=sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96 \
|
||||
--hash=sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6 \
|
||||
--hash=sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe \
|
||||
--hash=sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842 \
|
||||
--hash=sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa \
|
||||
--hash=sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3 \
|
||||
--hash=sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d \
|
||||
--hash=sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51 \
|
||||
--hash=sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7 \
|
||||
--hash=sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09 \
|
||||
--hash=sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096 \
|
||||
--hash=sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9 \
|
||||
--hash=sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b \
|
||||
--hash=sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5 \
|
||||
--hash=sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678 \
|
||||
--hash=sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea \
|
||||
--hash=sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d \
|
||||
--hash=sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49 \
|
||||
--hash=sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc \
|
||||
--hash=sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5 \
|
||||
--hash=sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027 \
|
||||
--hash=sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0 \
|
||||
--hash=sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878 \
|
||||
--hash=sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c \
|
||||
--hash=sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa \
|
||||
--hash=sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f \
|
||||
--hash=sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6 \
|
||||
--hash=sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2 \
|
||||
--hash=sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf \
|
||||
--hash=sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708 \
|
||||
--hash=sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6 \
|
||||
--hash=sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f \
|
||||
--hash=sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd \
|
||||
--hash=sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2 \
|
||||
--hash=sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d \
|
||||
--hash=sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7 \
|
||||
--hash=sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f \
|
||||
--hash=sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5 \
|
||||
--hash=sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6 \
|
||||
--hash=sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557 \
|
||||
--hash=sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14 \
|
||||
--hash=sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7 \
|
||||
--hash=sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd \
|
||||
--hash=sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c \
|
||||
--hash=sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17 \
|
||||
--hash=sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23 \
|
||||
--hash=sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db \
|
||||
--hash=sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6 \
|
||||
--hash=sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d \
|
||||
--hash=sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9 \
|
||||
--hash=sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee \
|
||||
--hash=sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6
|
||||
# via everest-testing (applications/utils/everest-testing/setup.cfg)
|
||||
131
tools/EVerest-main/applications/utils/requirements.bzl
Executable file
131
tools/EVerest-main/applications/utils/requirements.bzl
Executable file
@@ -0,0 +1,131 @@
|
||||
"""Starlark representation of locked requirements.
|
||||
|
||||
@generated by rules_python pip.parse bzlmod extension.
|
||||
"""
|
||||
|
||||
load("@rules_python//python:pip.bzl", "pip_utils")
|
||||
|
||||
all_requirements = [
|
||||
"@@rules_python++pip+everest-testing_pip_deps//attrs:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//backports_asyncio_runner:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//certifi:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//cffi:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//charset_normalizer:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//cryptography:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//exceptiongroup:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//idna:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//iniconfig:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jinja2:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jsonschema:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jsonschema_specifications:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//markupsafe:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//ocpp:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//packaging:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//paho_mqtt:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pluggy:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pycparser:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyftpdlib:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pygments:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyopenssl:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pytest:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pytest_asyncio:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//python_dateutil:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyyaml:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//referencing:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//requests:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//rpds_py:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//six:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//stringcase:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//tomli:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//typing_extensions:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//urllib3:pkg",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//websockets:pkg",
|
||||
]
|
||||
|
||||
all_whl_requirements_by_package = {
|
||||
"attrs": "@@rules_python++pip+everest-testing_pip_deps//attrs:whl",
|
||||
"backports_asyncio_runner": "@@rules_python++pip+everest-testing_pip_deps//backports_asyncio_runner:whl",
|
||||
"certifi": "@@rules_python++pip+everest-testing_pip_deps//certifi:whl",
|
||||
"cffi": "@@rules_python++pip+everest-testing_pip_deps//cffi:whl",
|
||||
"charset_normalizer": "@@rules_python++pip+everest-testing_pip_deps//charset_normalizer:whl",
|
||||
"cryptography": "@@rules_python++pip+everest-testing_pip_deps//cryptography:whl",
|
||||
"exceptiongroup": "@@rules_python++pip+everest-testing_pip_deps//exceptiongroup:whl",
|
||||
"idna": "@@rules_python++pip+everest-testing_pip_deps//idna:whl",
|
||||
"iniconfig": "@@rules_python++pip+everest-testing_pip_deps//iniconfig:whl",
|
||||
"jinja2": "@@rules_python++pip+everest-testing_pip_deps//jinja2:whl",
|
||||
"jsonschema": "@@rules_python++pip+everest-testing_pip_deps//jsonschema:whl",
|
||||
"jsonschema_specifications": "@@rules_python++pip+everest-testing_pip_deps//jsonschema_specifications:whl",
|
||||
"markupsafe": "@@rules_python++pip+everest-testing_pip_deps//markupsafe:whl",
|
||||
"ocpp": "@@rules_python++pip+everest-testing_pip_deps//ocpp:whl",
|
||||
"packaging": "@@rules_python++pip+everest-testing_pip_deps//packaging:whl",
|
||||
"paho_mqtt": "@@rules_python++pip+everest-testing_pip_deps//paho_mqtt:whl",
|
||||
"pluggy": "@@rules_python++pip+everest-testing_pip_deps//pluggy:whl",
|
||||
"pycparser": "@@rules_python++pip+everest-testing_pip_deps//pycparser:whl",
|
||||
"pyftpdlib": "@@rules_python++pip+everest-testing_pip_deps//pyftpdlib:whl",
|
||||
"pygments": "@@rules_python++pip+everest-testing_pip_deps//pygments:whl",
|
||||
"pyopenssl": "@@rules_python++pip+everest-testing_pip_deps//pyopenssl:whl",
|
||||
"pytest": "@@rules_python++pip+everest-testing_pip_deps//pytest:whl",
|
||||
"pytest_asyncio": "@@rules_python++pip+everest-testing_pip_deps//pytest_asyncio:whl",
|
||||
"python_dateutil": "@@rules_python++pip+everest-testing_pip_deps//python_dateutil:whl",
|
||||
"pyyaml": "@@rules_python++pip+everest-testing_pip_deps//pyyaml:whl",
|
||||
"referencing": "@@rules_python++pip+everest-testing_pip_deps//referencing:whl",
|
||||
"requests": "@@rules_python++pip+everest-testing_pip_deps//requests:whl",
|
||||
"rpds_py": "@@rules_python++pip+everest-testing_pip_deps//rpds_py:whl",
|
||||
"six": "@@rules_python++pip+everest-testing_pip_deps//six:whl",
|
||||
"stringcase": "@@rules_python++pip+everest-testing_pip_deps//stringcase:whl",
|
||||
"tomli": "@@rules_python++pip+everest-testing_pip_deps//tomli:whl",
|
||||
"typing_extensions": "@@rules_python++pip+everest-testing_pip_deps//typing_extensions:whl",
|
||||
"urllib3": "@@rules_python++pip+everest-testing_pip_deps//urllib3:whl",
|
||||
"websockets": "@@rules_python++pip+everest-testing_pip_deps//websockets:whl",
|
||||
}
|
||||
|
||||
all_whl_requirements = all_whl_requirements_by_package.values()
|
||||
|
||||
all_data_requirements = [
|
||||
"@@rules_python++pip+everest-testing_pip_deps//attrs:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//backports_asyncio_runner:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//certifi:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//cffi:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//charset_normalizer:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//cryptography:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//exceptiongroup:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//idna:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//iniconfig:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jinja2:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jsonschema:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//jsonschema_specifications:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//markupsafe:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//ocpp:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//packaging:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//paho_mqtt:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pluggy:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pycparser:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyftpdlib:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pygments:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyopenssl:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pytest:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pytest_asyncio:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//python_dateutil:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//pyyaml:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//referencing:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//requests:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//rpds_py:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//six:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//stringcase:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//tomli:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//typing_extensions:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//urllib3:data",
|
||||
"@@rules_python++pip+everest-testing_pip_deps//websockets:data",
|
||||
]
|
||||
|
||||
def requirement(name):
|
||||
return "@@rules_python++pip+everest-testing_pip_deps//{}:{}".format(pip_utils.normalize_name(name), "pkg")
|
||||
|
||||
def whl_requirement(name):
|
||||
return "@@rules_python++pip+everest-testing_pip_deps//{}:{}".format(pip_utils.normalize_name(name), "whl")
|
||||
|
||||
def data_requirement(name):
|
||||
return "@@rules_python++pip+everest-testing_pip_deps//{}:{}".format(pip_utils.normalize_name(name), "data")
|
||||
|
||||
def dist_info_requirement(name):
|
||||
return "@@rules_python++pip+everest-testing_pip_deps//{}:{}".format(pip_utils.normalize_name(name), "dist_info")
|
||||
17
tools/EVerest-main/applications/utils/scripts/README.md
Normal file
17
tools/EVerest-main/applications/utils/scripts/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Utility Scripts
|
||||
|
||||
This directory contains useful scripts for working with EVerest and meta-everest
|
||||
|
||||
_cargolock2bb.py_ converts a Cargo.lock file, that can also be loaded via an URL, into a .bb file
|
||||
|
||||
_check_dependency_versions.py_ parses a snapshot.yaml file and checks if there are new versions of the listed dependencies available
|
||||
|
||||
_config2cmake.py_ parses a EVerest yaml config and prints a CMake command line to only include the modules needed by this config
|
||||
|
||||
_create_snapshot.py_ uses EDM to create an snapshot in a temporary subdirectory and postprocesses it to fix common problems
|
||||
|
||||
_parsebb.py_ parses .bb files and returns a json object containing the repository link, branch, revision and direct link to a file relative to the repo link
|
||||
|
||||
_replace_license.py_ parses C++ files and replaces license headers with up2date Apache 2.0 headers used in EVerest
|
||||
|
||||
_snapshot2bb.py_ parses a snapshot.yaml file and modifies the corresponding recipe .bb files
|
||||
80
tools/EVerest-main/applications/utils/scripts/cargolock2bb.py
Executable file
80
tools/EVerest-main/applications/utils/scripts/cargolock2bb.py
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Convert Cargo.lock files (that can also be provided as an URL) into a .bb file
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import requests
|
||||
import toml
|
||||
|
||||
|
||||
def read_cargo_lock_file(cargo_lock_path):
|
||||
try:
|
||||
cargo_lock_file = open(cargo_lock_path, 'r')
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
with cargo_lock_file:
|
||||
return toml.load(cargo_lock_file)
|
||||
|
||||
|
||||
def read_cargo_lock_from_url(cargo_lock_url):
|
||||
try:
|
||||
req = requests.get(cargo_lock_url, timeout=5)
|
||||
return toml.loads(req.text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def print_bb_output(cargo_lock, skip_packages):
|
||||
if not cargo_lock or 'package' not in cargo_lock:
|
||||
print('# No packages in Cargo.lock file')
|
||||
return
|
||||
|
||||
print(f'SRC_URI += " \\')
|
||||
for package in cargo_lock['package']:
|
||||
if package['name'] in skip_packages:
|
||||
continue
|
||||
print(f' crate://crates.io/{package["name"]}/{package["version"]} \\')
|
||||
print(' "')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='converts Cargo.lock files to bitbake recipe output')
|
||||
|
||||
parser.add_argument('--input',
|
||||
dest='in_cargo_lock',
|
||||
default='Cargo.lock',
|
||||
help='Path to the Cargo.lock file')
|
||||
parser.add_argument('--url',
|
||||
dest='url_cargo_lock',
|
||||
default=None,
|
||||
help='URL to the Cargo.lock file')
|
||||
parser.add_argument('--skip',
|
||||
dest='in_skip',
|
||||
default='',
|
||||
type=str,
|
||||
help='List of packages to skip, using \',\' as delimiter.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
in_cargo_lock = os.path.realpath(os.path.expanduser(args.in_cargo_lock))
|
||||
in_cargo_lock_url = args.url_cargo_lock
|
||||
in_skip_packages = [entry for entry in args.in_skip.split(',')]
|
||||
|
||||
if in_cargo_lock_url:
|
||||
cargo_lock = read_cargo_lock_from_url(in_cargo_lock_url)
|
||||
else:
|
||||
cargo_lock = read_cargo_lock_file(in_cargo_lock)
|
||||
|
||||
print_bb_output(cargo_lock, in_skip_packages)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
124
tools/EVerest-main/applications/utils/scripts/check_dependency_versions.py
Executable file
124
tools/EVerest-main/applications/utils/scripts/check_dependency_versions.py
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Parse snapshot.yaml files check if dependency versions are up2date by checking for newer git tags
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
import subprocess
|
||||
from packaging.version import Version
|
||||
|
||||
def get_remote_tags(remote_url: str) -> list:
|
||||
"""Return the remote tags of the repo at path, or an empty list."""
|
||||
remote_tags = []
|
||||
try:
|
||||
result = subprocess.run(["git", "-c", "versionsort.suffix=-", "ls-remote", "--tags", "--sort=-v:refname", "--refs", "--quiet", remote_url],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
result_list = result.stdout.decode("utf-8").split("\n")
|
||||
for entry in result_list:
|
||||
ref_and_tag = entry.split("\t")
|
||||
if len(ref_and_tag) > 1:
|
||||
remote_tags.append(ref_and_tag[1].replace("refs/tags/", ""))
|
||||
except subprocess.CalledProcessError:
|
||||
return remote_tags
|
||||
|
||||
return remote_tags
|
||||
|
||||
|
||||
def parse_curl_version(curl_version):
|
||||
parsed_curl_version = curl_version.removeprefix("tiny-curl-")
|
||||
parsed_curl_version = parsed_curl_version.removeprefix("curl-")
|
||||
parsed_curl_version = parsed_curl_version.replace("_", ".")
|
||||
return parsed_curl_version
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='get updated dependency versions from snapshot.yaml')
|
||||
|
||||
parser.add_argument('--input',
|
||||
dest='in_snapshot',
|
||||
help='Path to the snapshot.yaml file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
in_snapshot = os.path.realpath(os.path.expanduser(args.in_snapshot))
|
||||
|
||||
snapshot = None
|
||||
with open(in_snapshot, encoding='utf-8') as snapshot_file:
|
||||
try:
|
||||
snapshot = yaml.safe_load(snapshot_file)
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing yaml of {in_snapshot}: {e}")
|
||||
return
|
||||
|
||||
if not snapshot:
|
||||
print(f"snapshot empty?")
|
||||
return
|
||||
|
||||
output = {}
|
||||
|
||||
branch_re = re.compile(r'branch=([^;|^"|\s]*)')
|
||||
|
||||
for key, entry in snapshot.items():
|
||||
branch = entry['branch']
|
||||
git_rev = entry['git_rev']
|
||||
git_tag = entry['git_tag']
|
||||
version = git_tag.removeprefix('wip-release-')
|
||||
version = version.removeprefix('v')
|
||||
version_without_rc, _vsep, _rc = version.partition('-rc')
|
||||
# todo: maybe even strip something like "wip-release-" from git_tag to keep versions sane?
|
||||
# or just do not modify the version if this cannot be parsed properly?
|
||||
# print(f"version: {version} without-rc: {version_without_rc}")
|
||||
git_url = entry['git']
|
||||
remote_tags = get_remote_tags(git_url)
|
||||
tags = []
|
||||
for original_remote_tag in remote_tags:
|
||||
remote_tag = original_remote_tag.removeprefix("v")
|
||||
if key == "libcurl":
|
||||
# special handling for curl
|
||||
remote_tag = parse_curl_version(remote_tag)
|
||||
if key == "pugixml":
|
||||
if remote_tag == "latest":
|
||||
continue
|
||||
|
||||
tags.append((original_remote_tag, remote_tag))
|
||||
|
||||
remote_versions = []
|
||||
version_mapping = {}
|
||||
for (original_remote_tag, remote_tag) in tags:
|
||||
try:
|
||||
ver = Version(remote_tag)
|
||||
version_mapping[ver] = original_remote_tag
|
||||
remote_versions.append(ver)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
remote_versions.sort(reverse=True)
|
||||
|
||||
try:
|
||||
remote_version = remote_versions[0]
|
||||
parsed_version = ""
|
||||
if key == "libcurl":
|
||||
parsed_version = Version(parse_curl_version(version))
|
||||
else:
|
||||
parsed_version = Version(version)
|
||||
if parsed_version != remote_version:
|
||||
print(f" {key}: remote version is different from version in snapshot! Check if you want to update.")
|
||||
print(f" snapshot version: {version}")
|
||||
print(f" remote version: {remote_version}, original tag: {version_mapping[remote_version]}")
|
||||
except Exception as e:
|
||||
print(f" {key}: cannot parse remote version, please check manually!")
|
||||
print(f" snapshot version: {version}")
|
||||
print(f" thrown exception: {e}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
66
tools/EVerest-main/applications/utils/scripts/config2cmake.py
Executable file
66
tools/EVerest-main/applications/utils/scripts/config2cmake.py
Executable file
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Parse a EVerest config and return a CMake command line to build only those modules
|
||||
"""
|
||||
import argparse
|
||||
import yaml
|
||||
import sys
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_modules(config_yaml_path: Path):
|
||||
try:
|
||||
config = yaml.safe_load(config_yaml_path.read_text())
|
||||
module_names = set()
|
||||
for _key, value in config['active_modules'].items():
|
||||
module_names.add(value['module'])
|
||||
modules = ';'.join(sorted(module_names))
|
||||
return f'-DEVEREST_INCLUDE_MODULES="{modules}"'
|
||||
except yaml.YAMLError as err:
|
||||
raise Exception(f'Could not parse config file {config_yaml_path}') from err
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description='parse EVerest configs and extract modules')
|
||||
|
||||
parser.add_argument('config',
|
||||
help='Path to EVerest config',
|
||||
nargs=1)
|
||||
parser.add_argument('--full',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help='Set this flag if you want a full cmake command line (with "build" as default build-dir)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
config_path = Path(args.config[0]).expanduser().resolve()
|
||||
|
||||
try:
|
||||
modules = get_modules(config_path)
|
||||
if args.full:
|
||||
print(f'cmake -S . -B build {modules}')
|
||||
else:
|
||||
print(modules)
|
||||
except Exception as err:
|
||||
print(f'Could not generate CMake command line: {err}')
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
return_value = 1
|
||||
try:
|
||||
return_value = main()
|
||||
except Exception as e:
|
||||
print(f'Error: {e}')
|
||||
return_value = 1
|
||||
sys.exit(return_value)
|
||||
149
tools/EVerest-main/applications/utils/scripts/create_snapshot.py
Executable file
149
tools/EVerest-main/applications/utils/scripts/create_snapshot.py
Executable file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Use edm to create a snapshot of the current directory without polluting the current working dir
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import yaml
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import string
|
||||
|
||||
|
||||
def get_tags(path: Path) -> list:
|
||||
"""Return a list of tags the HEAD points to of the repo at path, or an empty list."""
|
||||
tags = []
|
||||
try:
|
||||
result = subprocess.run(["git", "-C", path, "tag", "--points-at", "HEAD"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
|
||||
tags = result.stdout.decode("utf-8").splitlines()
|
||||
except subprocess.CalledProcessError:
|
||||
return tag
|
||||
|
||||
return tags
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='create an isolated snapshot with edm')
|
||||
|
||||
parser.add_argument('--working-dir', '-wd', type=str,
|
||||
help='Working directory containing the EVerest workspace (default: .)', default=str(Path.cwd()))
|
||||
parser.add_argument('--temp-dir', '-td', type=str,
|
||||
help='Temporary directory for creating the snapshot in (default: working-dir/tmp-for-snapshot)', default=None)
|
||||
parser.add_argument('--version', type=str,
|
||||
help='dependency version to override, format is: dependency1:version,dependency2:version2', default=None)
|
||||
parser.add_argument('--git-version', action='store_true', help='Use "git" as version when encountering a git hash')
|
||||
parser.add_argument('--allow-relative-to-working-dir', action='store_true', help='Allow temporary directory to be relative to working dir (dangerous!)')
|
||||
parser.add_argument('--post-process', action='store_true', help='Postprocess existing snapshot')
|
||||
parser.add_argument('--include-external-deps', action='store_true', help='Include external dependencies in snapshot')
|
||||
parser.add_argument('--exclude-dir', action='append', dest='excluded_dirs', type=str, help='Exclude specified directory from snapshot (can be used multiple times)', default=[])
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
working_dir = Path(args.working_dir).expanduser().resolve()
|
||||
|
||||
tmp_dir = working_dir / 'tmp-for-snapshot'
|
||||
|
||||
if args.temp_dir:
|
||||
tmp_dir = Path(args.temp_dir).expanduser().resolve()
|
||||
|
||||
if working_dir == tmp_dir:
|
||||
print(f'Temporary directory cannot be equal to working directory: {tmp_dir}')
|
||||
return 1
|
||||
|
||||
if tmp_dir.is_relative_to(working_dir) and tmp_dir.parent != working_dir and not args.allow_relative_to_working_dir:
|
||||
print(f'Temporary directory cannot be relative to working directory: {tmp_dir}')
|
||||
return 1
|
||||
|
||||
excluded_paths = []
|
||||
for excluded_dir in args.excluded_dirs:
|
||||
excluded_path = working_dir / excluded_dir
|
||||
excluded_path = excluded_path.expanduser().resolve()
|
||||
excluded_paths.append(excluded_path)
|
||||
|
||||
if not args.post_process and tmp_dir.exists():
|
||||
print(f'Temporary directory dir already exists, deleting it: {tmp_dir}')
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
if not args.post_process:
|
||||
tmp_dir.mkdir()
|
||||
|
||||
subdirs = list(working_dir.glob('*/'))
|
||||
for subdir in subdirs:
|
||||
subdir_path = Path(subdir)
|
||||
if not subdir_path.is_dir():
|
||||
print(f'{subdir_path} is not a dir, ignoring')
|
||||
continue
|
||||
if subdir_path == tmp_dir:
|
||||
print(f'{subdir_path} is tmp dir, ignoring')
|
||||
continue
|
||||
if subdir_path in excluded_paths:
|
||||
print(f'{subdir_path} is excluded, ignoring')
|
||||
continue
|
||||
print(f'Copying {subdir_path} to {tmp_dir}')
|
||||
destdir = tmp_dir / subdir_path.name
|
||||
|
||||
shutil.copytree(subdir_path, destdir, ignore=shutil.ignore_patterns('build*'))
|
||||
|
||||
print('Running edm snaphot --recursive')
|
||||
cmd_line = ['edm']
|
||||
if args.include_external_deps:
|
||||
cmd_line.append('--external-in-config')
|
||||
cmd_line.extend(['snapshot', '--recursive'])
|
||||
with subprocess.Popen(cmd_line, stderr=subprocess.PIPE, cwd=tmp_dir) as edm:
|
||||
for line in edm.stderr:
|
||||
print(line.decode('utf-8'), end='')
|
||||
in_snapshot = tmp_dir / 'snapshot.yaml'
|
||||
snapshot = None
|
||||
with open(in_snapshot, mode='r', encoding='utf-8') as snapshot_file:
|
||||
try:
|
||||
snapshot = yaml.safe_load(snapshot_file)
|
||||
except yaml.YAMLError as e:
|
||||
print(f'Error parsing yaml of {in_snapshot}: {e}')
|
||||
if snapshot:
|
||||
# check if git_tag is a 40 character hex string and assume it is a git_rev
|
||||
if args.git_version:
|
||||
for dependency, entry in snapshot.items():
|
||||
git_tag = ''
|
||||
if 'git_tag' in entry:
|
||||
git_tag = entry['git_tag']
|
||||
elif 'git_rev' in entry:
|
||||
git_tag = entry['git_rev']
|
||||
if len(git_tag) == 40 and all(character in string.hexdigits for character in git_tag):
|
||||
snapshot[dependency]['git_tag'] = 'git'
|
||||
if args.version:
|
||||
versions = args.version.split(',')
|
||||
for dep_versions in versions:
|
||||
dependency, version = dep_versions.split(':')
|
||||
if dependency in snapshot:
|
||||
print(f'Overriding {dependency} version {snapshot[dependency]["git_tag"]} to {version}')
|
||||
snapshot[dependency]['git_tag'] = version
|
||||
for dependency, entry in snapshot.items():
|
||||
git_tag = ''
|
||||
if 'git_tag' in entry:
|
||||
git_tag = entry['git_tag']
|
||||
if git_tag == 'latest':
|
||||
print(f'{dependency} has tag "latest", check if there is a version tag as well')
|
||||
dependency_path = tmp_dir / dependency
|
||||
tags = get_tags(dependency_path)
|
||||
tags.remove('latest')
|
||||
if len(tags) == 1:
|
||||
print(' Fixing "latest" tag in snapshot')
|
||||
snapshot[dependency]['git_tag'] = tags[0]
|
||||
else:
|
||||
print(f' List of tags with "latest" removed: "{tags}" is not directly usable...')
|
||||
|
||||
with open(in_snapshot, mode='w', encoding='utf-8') as snapshot_file:
|
||||
yaml.safe_dump(snapshot, snapshot_file, indent=2, sort_keys=False, width=120)
|
||||
print('Done')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
137
tools/EVerest-main/applications/utils/scripts/fix_dco_signoff.py
Executable file
137
tools/EVerest-main/applications/utils/scripts/fix_dco_signoff.py
Executable file
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: andreas.heinrich@rwth-aachen.de
|
||||
Fix author and signoff of commits to match DCO requirements.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
|
||||
def get_git_author_string(args: argparse.Namespace) -> str:
|
||||
try:
|
||||
name = subprocess.run(
|
||||
['git', '-C', str(args.git_root), 'config', 'user.name'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
).stdout.strip()
|
||||
|
||||
email = subprocess.run(
|
||||
['git', '-C', str(args.git_root), 'config', 'user.email'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
).stdout.strip()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f'Git config failed with error: {e.stderr}')
|
||||
exit(1)
|
||||
|
||||
result = f"{name} <{email}>"
|
||||
return result
|
||||
|
||||
|
||||
def remove_signoffs(args: argparse.Namespace) -> str:
|
||||
try:
|
||||
msg_result = subprocess.run(
|
||||
['git', '-C', str(args.git_root), 'log', '-1', '--pretty=%B'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f'Git log failed with error: {e.stderr}')
|
||||
exit(1)
|
||||
|
||||
current_msg = msg_result.stdout
|
||||
lines = current_msg.splitlines()
|
||||
filtered_lines = [
|
||||
line for line in lines
|
||||
if not line.strip().startswith('Signed-off-by:')
|
||||
]
|
||||
clean_msg = "\n".join(filtered_lines).strip()
|
||||
return clean_msg
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Fix author and signoff of latest commit to match DCO requirements.')
|
||||
|
||||
parser.add_argument(
|
||||
'--author', '-a',
|
||||
type=str,
|
||||
help='Author name and email in the format "Name <email>"',
|
||||
default=None
|
||||
)
|
||||
parser.add_argument(
|
||||
'--git-root', '-g',
|
||||
type=str,
|
||||
help='Git root directory (default: .)',
|
||||
default=str(Path.cwd())
|
||||
)
|
||||
parser.add_argument(
|
||||
'--remove-existing-signoffs', '--rm',
|
||||
action='store_true',
|
||||
help='Remove existing Signed-off-by trailers before adding the new one',
|
||||
default=False
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
args.git_root = Path(args.git_root).expanduser().resolve()
|
||||
|
||||
if args.author is None:
|
||||
print(
|
||||
"\033[93m"
|
||||
"Warning:\n"
|
||||
" You are overriding the author information with yourself as author.\n"
|
||||
" Please make sure that you are the original author of the commit\n"
|
||||
" or give credits to the original author in commit message by for example\n"
|
||||
" adding a 'Co-authored-by: Original Author <email>' trailer to the commit message.\n"
|
||||
"\033[0m"
|
||||
)
|
||||
args.author = get_git_author_string(args)
|
||||
else:
|
||||
print(
|
||||
"\033[93m"
|
||||
"Warning:\n"
|
||||
" You are overriding the author information with a custom value.\n"
|
||||
" You are not allowed to signoff a commit for another person,\n"
|
||||
" except if you are the original author and have the right to do so\n"
|
||||
" or if you are a maintainer fixing the commit for the original author.\n"
|
||||
" For example in case of mismatching user.name and user.email in\n"
|
||||
" case of using github's web based git operations"
|
||||
"\033[0m"
|
||||
)
|
||||
|
||||
|
||||
subprocess_args = [
|
||||
'git', '-C', str(args.git_root),
|
||||
'commit', '--amend', '--no-edit',
|
||||
'--author', args.author,
|
||||
'--trailer', 'Signed-off-by: ' + args.author
|
||||
]
|
||||
if args.remove_existing_signoffs:
|
||||
clean_msg = remove_signoffs(args)
|
||||
subprocess_args.extend(['-m', clean_msg])
|
||||
try:
|
||||
subprocess.run(
|
||||
subprocess_args,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f'Git amend failed with error:: {e.stderr}')
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
67
tools/EVerest-main/applications/utils/scripts/parsebb.py
Executable file
67
tools/EVerest-main/applications/utils/scripts/parsebb.py
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Parse .bb files and extract git information as json
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='extracts git url and rev from .bb files')
|
||||
|
||||
parser.add_argument('--input',
|
||||
dest='in_bb',
|
||||
help='Path to the .bb file')
|
||||
parser.add_argument('--file',
|
||||
dest='in_file',
|
||||
help='Relative path to the repo')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
in_bb = os.path.realpath(os.path.expanduser(args.in_bb))
|
||||
in_rel_path = args.in_file
|
||||
|
||||
bb = None
|
||||
try:
|
||||
bb_file = open(in_bb, 'r')
|
||||
except Exception:
|
||||
return
|
||||
|
||||
output = {}
|
||||
|
||||
with bb_file as f:
|
||||
bb = f.read()
|
||||
bbvars = re.findall(r'^.+=[^=]+\n', bb, flags=re.MULTILINE)
|
||||
for variable in bbvars:
|
||||
variable_search = re.search(r'([a-zA-Z_]*)\s?=\s?\"([^\s]*)', variable)
|
||||
if variable_search:
|
||||
name = variable_search.group(1)
|
||||
value = variable_search.group(2)
|
||||
if name == 'SRC_URI':
|
||||
src_uri_search = re.search(r'git:\/\/([^;]*);branch=([^;]*)', value)
|
||||
if src_uri_search:
|
||||
repo = f'https://{src_uri_search.group(1)}'
|
||||
branch = src_uri_search.group(2)
|
||||
output['repo'] = repo
|
||||
output['branch'] = branch
|
||||
if name == 'SRCREV':
|
||||
src_rev_search = re.search(r'([^;]*)\"', value)
|
||||
if src_rev_search:
|
||||
rev = src_rev_search.group(1)
|
||||
output['rev'] = rev
|
||||
output['url'] = output['repo'].replace('.git', '/').replace('github.com/', 'raw.githubusercontent.com/')
|
||||
output['url'] += output['rev'] + '/' + in_rel_path
|
||||
print(f'{json.dumps(output)}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
80
tools/EVerest-main/applications/utils/scripts/replace_license.py
Executable file
80
tools/EVerest-main/applications/utils/scripts/replace_license.py
Executable file
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Replace licenses with Apache 2.0
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='replaces licenses with Apache 2.0')
|
||||
|
||||
parser.add_argument('--working-dir', '-wd', type=str,
|
||||
help='Working directory (default: .)', default=str(Path.cwd()))
|
||||
parser.add_argument('--no-year', action='store_true', help='Do not include years in license header')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
working_dir = Path(args.working_dir).expanduser().resolve()
|
||||
|
||||
files = [file for file in working_dir.rglob('*') if file.suffix in ['.cpp', '.hpp']]
|
||||
|
||||
year = ''
|
||||
if not args.no_year:
|
||||
year = f'2020 - {date.today().year} '
|
||||
|
||||
license_text = f"""// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright {year}Pionix GmbH and Contributors to EVerest
|
||||
"""
|
||||
|
||||
success = 0
|
||||
failure = 0
|
||||
count = len(files)
|
||||
|
||||
for file in files:
|
||||
content = file.read_text()
|
||||
if content.startswith('/*'):
|
||||
needle = '*/\n'
|
||||
end = content.find(needle) + len(needle)
|
||||
new_content = license_text + content[end:]
|
||||
file.write_text(new_content)
|
||||
print(f'Modified {file} with new license header')
|
||||
success += 1
|
||||
else:
|
||||
content_lines = content.splitlines()
|
||||
end = 0
|
||||
for line in content_lines:
|
||||
if line.startswith('//'):
|
||||
end += 1
|
||||
else:
|
||||
break
|
||||
new_content = license_text + '\n'.join(content_lines[end:]) + '\n'
|
||||
file.write_text(new_content)
|
||||
success += 1
|
||||
|
||||
if success != count:
|
||||
print('ERROR during license replacement')
|
||||
else:
|
||||
print('Everything went well')
|
||||
|
||||
manifest_files = [file for file in working_dir.rglob('*') if file.name == 'manifest.yaml']
|
||||
for file in manifest_files:
|
||||
manifest = file.read_text()
|
||||
needle = 'license:'
|
||||
start = manifest.find(needle)
|
||||
end = manifest.find('\n', start+len(needle))
|
||||
new_manifest = manifest[:start] + 'license: https://opensource.org/licenses/Apache-2.0' + manifest[end:]
|
||||
file.write_text(new_manifest)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
118
tools/EVerest-main/applications/utils/scripts/snapshot2bb.py
Executable file
118
tools/EVerest-main/applications/utils/scripts/snapshot2bb.py
Executable file
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
#
|
||||
"""
|
||||
author: kai-uwe.hermann@pionix.de
|
||||
Parse snapshot.yaml files and modify corresponding .bb recipes
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='modify .bb files based on a snapshot')
|
||||
|
||||
parser.add_argument('--input',
|
||||
dest='in_snapshot',
|
||||
help='Path to the snapshot.yaml file')
|
||||
parser.add_argument('--out',
|
||||
dest='out_dir',
|
||||
help='Relative path to meta-everest')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
in_snapshot = os.path.realpath(os.path.expanduser(args.in_snapshot))
|
||||
out_dir = Path(os.path.realpath(os.path.expanduser(args.out_dir)))
|
||||
|
||||
snapshot = None
|
||||
with open(in_snapshot, encoding='utf-8') as snapshot_file:
|
||||
try:
|
||||
snapshot = yaml.safe_load(snapshot_file)
|
||||
except yaml.YAMLError as e:
|
||||
print(f"Error parsing yaml of {in_snapshot}: {e}")
|
||||
return
|
||||
|
||||
if not snapshot:
|
||||
print(f"snapshot empty?")
|
||||
return
|
||||
|
||||
output = {}
|
||||
|
||||
print(f"got snapshot: {snapshot}")
|
||||
|
||||
recipes_core = out_dir / 'recipes-core'
|
||||
if not recipes_core.exists():
|
||||
print(f"cannot find recipes-core in {recipes_core}")
|
||||
return
|
||||
|
||||
# TODO: provide this via a file as a command line parameter
|
||||
mapping = {'Josev': 'josev/python3-iso15118',
|
||||
'everest-utils': 'everest-devtools/evcli',
|
||||
'EVerest': 'everest/everest-core',
|
||||
'everest-framework': 'everest/everest-framework',
|
||||
'everest-sqlite': 'everest/everest-sqlite',
|
||||
'libcbv2g': 'everest/libcbv2g',
|
||||
'libevse-security': 'everest/libevse-security',
|
||||
'libfsm': 'everest/libfsm',
|
||||
'libiso15118': 'everest/libiso15118',
|
||||
'liblog': 'everest/liblog',
|
||||
'libnfc-nci': 'everest/libnfc-nci',
|
||||
'libocpp': 'everest/libocpp',
|
||||
'libslac': 'everest/libslac',
|
||||
'libtimer': 'everest/libtimer'}
|
||||
|
||||
branch_re = re.compile(r'branch=([^;|^"|\s]*)')
|
||||
|
||||
for key, entry in snapshot.items():
|
||||
print(f"key: {key}")
|
||||
branch = entry['branch']
|
||||
git_rev = entry['git_rev']
|
||||
git_tag = entry['git_tag']
|
||||
version = git_tag.removeprefix('wip-release-')
|
||||
version = version.removeprefix('v')
|
||||
version_without_rc, _vsep, _rc = version.partition('-')
|
||||
# todo: maybe even strip something like "wip-release-" from git_tag to keep versions sane?
|
||||
# or just do not modify the version if this cannot be parsed properly?
|
||||
print(f"version: {version} without-rc: {version_without_rc}")
|
||||
if key in mapping:
|
||||
file_glob = f'{mapping[key]}_*.bb'
|
||||
files = list(recipes_core.glob(file_glob))
|
||||
if len(files) == 1:
|
||||
bb_path = files[0]
|
||||
|
||||
print(f"found a matching file: {bb_path}")
|
||||
with open(bb_path) as bb_file:
|
||||
bb_content = bb_file.read().splitlines()
|
||||
bb_content_modified = []
|
||||
for index, line in enumerate(bb_content):
|
||||
if line.startswith("SRC_URI"):
|
||||
def replace(match):
|
||||
return f'branch={branch}'
|
||||
line = branch_re.sub(replace, line)
|
||||
elif line.startswith("SRCREV"):
|
||||
line = f"SRCREV = \"{git_rev}\""
|
||||
|
||||
bb_content_modified.append(line)
|
||||
# create new filename
|
||||
prefix, sep, postfix = bb_path.name.partition('_')
|
||||
new_filename = f'{prefix}_{version_without_rc}.bb'
|
||||
new_bb_path = bb_path.parent / new_filename
|
||||
if bb_path != new_bb_path:
|
||||
print(f"filename is different, so remove the old one")
|
||||
bb_path.unlink(missing_ok=True)
|
||||
|
||||
print(f"writing bb file: {new_bb_path}")
|
||||
with open(new_bb_path, mode='w+') as new_bb_file:
|
||||
new_bb_file.write('\n'.join(bb_content_modified))
|
||||
new_bb_file.write('\n')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user