Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

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

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

View File

@@ -0,0 +1,5 @@
load("//modules:module.bzl", "py_everest_module")
py_everest_module(
name = "PyEvJosev",
)

View File

@@ -0,0 +1,70 @@
description: >-
This module implements an DIN70121, ISO15118-2 and ISO15118-20 EV using the Josev project.
config:
device:
description: >-
Ethernet device used for HLC. Any local interface that has an ipv6 link-local and a MAC addr will work.
type: string
default: eth0
supported_DIN70121:
description: The EV supports the DIN SPEC
type: boolean
default: false
supported_ISO15118_2:
description: The EV supports ISO15118-2
type: boolean
default: false
supported_ISO15118_20_AC:
description: The EV supports ISO15118-20 AC
type: boolean
default: false
supported_ISO15118_20_DC:
description: The EV supports ISO15118-20 DC
type: boolean
default: false
tls_active:
description: If true, EVCC connects to SECC as TLS client
type: boolean
default: false
enforce_tls:
description: The EVCC will enforce a TLS connection
type: boolean
default: false
is_cert_install_needed:
description: >-
If true, the contract certificate will be installed via the evse.
And any existing contract certificate will also be overwritten.
type: boolean
default: false
enable_tls_1_3:
description: The EVCC will enable TLS version 1.3
type: boolean
default: false
is_internet_service_needed:
description: If true, the ev will ask for internet service
type: boolean
default: false
request_all_service_details:
description: If true, the ev will ask for details about all offered services
type: boolean
default: false
select_all_vas_services:
description: If true, the ev will select all offered services
type: boolean
default: false
supported_d20_energy_services:
description: >-
The supported ISO15118-20 energy services (DC, DC_BPT, AC, AC_BPT) the EV supports,
provided as a prioritized list. The first entry in the list has the highest priority
and is the most likely to be selected. The services should be separated only with commas.
type: string
default: "DC,DC_BPT"
provides:
ev:
interface: ISO15118_ev
description: This module implements the ISO15118-2 implementation of an EV
enable_external_mqtt: true
metadata:
license: https://opensource.org/licenses/Apache-2.0
authors:
- Sebastian Lukas

View File

@@ -0,0 +1,156 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
import asyncio
import sys
from pathlib import Path
import threading
import math
from everest.framework import Module, RuntimeSession, log
# fmt: off
JOSEV_WORK_DIR = Path(__file__).parent / '../../3rd_party/josev'
sys.path.append(JOSEV_WORK_DIR.as_posix())
from iso15118.evcc import EVCCHandler
from iso15118.evcc.controller.simulator import SimEVController
from iso15118.evcc.evcc_config import EVCCConfig
from iso15118.evcc.everest import context as JOSEV_CONTEXT
from iso15118.shared.exificient_exi_codec import ExificientEXICodec
from iso15118.shared.settings import set_PKI_PATH, enable_tls_1_3
from utilities import (
setup_everest_logging,
determine_network_interface,
patch_josev_config
)
setup_everest_logging()
EVEREST_CERTS_SUB_DIR = 'certs'
async def evcc_handler_main_loop(module_config: dict, exi_codec: ExificientEXICodec):
"""
Entrypoint function that starts the ISO 15118 code running on
the EVCC (EV Communication Controller)
"""
iface = determine_network_interface(module_config['device'])
evcc_config = EVCCConfig()
patch_josev_config(evcc_config, module_config)
await EVCCHandler(
evcc_config=evcc_config,
iface=iface,
exi_codec=exi_codec,
ev_controller=SimEVController(evcc_config),
).start()
class PyEVJosevModule():
def __init__(self) -> None:
self._es = JOSEV_CONTEXT.ev_state
self._session = RuntimeSession()
m = Module(self._session)
log.update_process_name(m.info.id)
self._setup = m.say_hello()
etc_certs_path = m.info.paths.etc / EVEREST_CERTS_SUB_DIR
set_PKI_PATH(str(etc_certs_path.resolve()))
if self._setup.configs.module['enable_tls_1_3']:
enable_tls_1_3()
self._es.internet_service_needed = self._setup.configs.module['is_internet_service_needed']
self._es.all_service_details = self._setup.configs.module['request_all_service_details']
self._es.all_vas_services = self._setup.configs.module['select_all_vas_services']
# setup publishing callback
def publish_callback(variable_name: str, value: any):
m.publish_variable('ev', variable_name, value)
# set publish callback for context
JOSEV_CONTEXT.set_publish_callback(publish_callback)
# setup handlers
for cmd in m.implementations['ev'].commands:
m.implement_command(
'ev', cmd, getattr(self, f'_handler_{cmd}'))
# init ready event
self._ready_event = threading.Event()
self._mod = m
self._mod.init_done(self._ready)
def start_evcc_handler(self):
exi_codec = ExificientEXICodec()
try:
while True:
self._ready_event.wait()
try:
asyncio.run(evcc_handler_main_loop(self._setup.configs.module, exi_codec))
self._mod.publish_variable('ev', 'v2g_session_finished', None)
except KeyboardInterrupt:
log.debug("SECC program terminated manually")
break
finally:
self._ready_event.clear()
finally:
exi_codec.shutdown()
def _ready(self):
log.debug("ready!")
# implementation handlers
def _handler_start_charging(self, args) -> bool:
self._es.DepartureTime = args['DepartureTime']
self._es.EAmount_kWh = args['EAmount']
self._es.EnergyTransferMode = args['EnergyTransferMode']
if "payment_option" in args['SelectedPaymentOption']:
self._es.PaymentOption = args['SelectedPaymentOption']['payment_option']
if "enforce_payment_option" in args['SelectedPaymentOption']:
self._es.enforce_payment_option = args['SelectedPaymentOption']['enforce_payment_option']
self._ready_event.set()
return True
def _handler_stop_charging(self, args):
self._es.StopCharging = True
def _handler_pause_charging(self, args):
self._es.Pause = True
def _handler_set_fault(self, args):
pass
def _handler_set_dc_params(self, args):
parameters = args['EvParameters']
self._es.dc_max_current_limit = parameters['max_current_limit']
self._es.dc_max_power_limit = parameters['max_power_limit']
self._es.dc_max_voltage_limit = parameters['max_voltage_limit']
self._es.dc_energy_capacity = parameters['energy_capacity']
self._es.dc_target_current = parameters['target_current']
self._es.dc_target_voltage = parameters['target_voltage']
def _handler_set_bpt_dc_params(self, args):
parameters = args['EvBPTParameters']
self._es.dc_discharge_max_current_limit = parameters["discharge_max_current_limit"]
self._es.dc_discharge_max_power_limit = parameters['discharge_max_power_limit']
self._es.dc_discharge_target_current = parameters['discharge_target_current']
self._es.minimal_soc = parameters["discharge_minimal_soc"]
def _handler_enable_sae_j2847_v2g_v2h(self, args):
self._es.SAEJ2847_V2H_V2G_Active = True
def _handler_update_soc(self, args):
self._es.actual_soc = math.floor(args['SoC'])
py_ev_josev = PyEVJosevModule()
py_ev_josev.start_evcc_handler()

View File

@@ -0,0 +1,107 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
import logging
import netifaces
from everest.framework import log
from iso15118.evcc.evcc_config import EVCCConfig
from iso15118.shared.utils import load_requested_protocols, load_requested_energy_services
class EverestPyLoggingHandler(logging.Handler):
def __init__(self):
logging.Handler.__init__(self)
def emit(self, record):
msg = self.format(record)
log_level: int = record.levelno
if log_level == logging.CRITICAL:
log.critical(msg)
elif log_level == logging.ERROR:
log.error(msg)
elif log_level == logging.WARNING:
log.warning(msg)
# FIXME (aw): implicitely pipe everything with loglevel INFO into DEBUG
else:
log.debug(msg)
def setup_everest_logging():
# remove all logging handler so that we'll have only our custom one
# FIXME (aw): this is probably bad practice because if everyone does that, only the last one might survive
logging.getLogger().handlers.clear()
handler = EverestPyLoggingHandler()
# NOTE (aw): the default formatting should be fine
# formatter = logging.Formatter("%(levelname)s - %(name)s (%(lineno)d): %(message)s")
# handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)
def choose_first_ipv6_local() -> str:
for iface in netifaces.interfaces():
if netifaces.AF_INET6 in netifaces.ifaddresses(iface):
for netif_inet6 in netifaces.ifaddresses(iface)[netifaces.AF_INET6]:
if 'fe80' in netif_inet6['addr']:
return iface
log.warning('No necessary IPv6 link-local address was found!')
return 'eth0'
def determine_network_interface(preferred_interface: str) -> str:
if preferred_interface == "auto":
return choose_first_ipv6_local()
elif preferred_interface not in netifaces.interfaces():
log.warning(
f"The network interface {preferred_interface} was not found!")
return preferred_interface
def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None:
josev_config.use_tls = everest_config['tls_active']
josev_config.enforce_tls = everest_config['enforce_tls']
josev_config.is_cert_install_needed = everest_config['is_cert_install_needed']
josev_config.sdp_retry_cycles = 1
protocols = [
"DIN_SPEC_70121",
"ISO_15118_2",
"ISO_15118_20_AC",
"ISO_15118_20_DC",
]
if not everest_config['supported_DIN70121']:
protocols.remove('DIN_SPEC_70121')
if not everest_config['supported_ISO15118_2']:
protocols.remove('ISO_15118_2')
if not everest_config['supported_ISO15118_20_AC']:
protocols.remove('ISO_15118_20_AC')
if not everest_config['supported_ISO15118_20_DC']:
protocols.remove('ISO_15118_20_DC')
if not protocols:
log.error("The supporting hlc protocols were not specified")
josev_config.supported_protocols = load_requested_protocols(protocols)
if everest_config['supported_d20_energy_services']:
josev_config.supported_energy_services = load_requested_energy_services(
everest_config['supported_d20_energy_services'].split(',')
)
else:
josev_config.supported_energy_services = load_requested_energy_services(
['DC']
)