Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integration of the EVCC from Josev into EVerest #13

Merged
merged 1 commit into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,9 @@ install(
install(
FILES iso15118/__init__.py
DESTINATION "${THIRD_PARTY_APP_DST}/josev/iso15118"
)
)

install(
DIRECTORY iso15118/evcc/
DESTINATION "${THIRD_PARTY_APP_DST}/josev/iso15118/evcc"
)
1 change: 1 addition & 0 deletions iso15118/evcc/comm_session_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ async def get_from_rcv_queue(self, queue: asyncio.Queue):
except SDPFailedError as exc:
logger.exception(exc)
# TODO not sure what else to do here
break
else:
logger.warning(
"Communication session handler received "
Expand Down
3 changes: 2 additions & 1 deletion iso15118/evcc/controller/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
DCEVStatus,
EnergyTransferModeEnum,
SAScheduleTuple,
CertificateChain,
)
from iso15118.shared.messages.iso15118_20.ac import (
ACChargeParameterDiscoveryReqParams,
Expand Down Expand Up @@ -340,7 +341,7 @@ async def continue_charging(self) -> bool:

@abstractmethod
async def store_contract_cert_and_priv_key(
self, contract_cert: bytes, priv_key: bytes
self, contract_cert_chain: CertificateChain, priv_key: bytes
):
"""
Stores the contract certificate and associated private key, both needed
Expand Down
86 changes: 70 additions & 16 deletions iso15118/evcc/controller/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@
import logging
import random
from typing import List, Optional, Tuple, Union
import os
from pathlib import Path

from cryptography.x509 import load_der_x509_certificate, ExtensionOID
from cryptography.hazmat.primitives.serialization import (
BestAvailableEncryption,
Encoding,
PrivateFormat,
)

from iso15118.evcc import EVCCConfig
from iso15118.evcc.controller.interface import ChargeParamsV2, EVControllerInterface
from iso15118.shared.exceptions import InvalidProtocolError, MACAddressNotFound
from iso15118.shared.exceptions import (
InvalidProtocolError,
MACAddressNotFound,
CertChainLengthError,
)
from iso15118.shared.messages.datatypes import (
DCEVChargeParams,
PVEAmount,
Expand Down Expand Up @@ -59,6 +72,7 @@
DCEVStatus,
ProfileEntryDetails,
SAScheduleTuple,
CertificateChain,
)
from iso15118.shared.messages.iso15118_20.ac import (
ACChargeParameterDiscoveryReqParams,
Expand Down Expand Up @@ -104,8 +118,20 @@
)
from iso15118.shared.network import get_nic_mac_address

from iso15118.shared.security import (
CertPath,
KeyEncoding,
KeyPasswordPath,
KeyPath,
to_ec_priv_key,
)

from iso15118.shared.settings import get_PKI_PATH

logger = logging.getLogger(__name__)

from iso15118.evcc.everest import context as EVEREST_CONTEXT
EVEREST_EV_STATE = EVEREST_CONTEXT.ev_state

class SimEVController(EVControllerInterface):
"""
Expand Down Expand Up @@ -135,7 +161,7 @@ def __init__(self, evcc_config: EVCCConfig):
multiplier=0, value=1, unit=UnitSymbol.AMPERE
),
dc_target_voltage=PVEVTargetVoltage(
multiplier=0, value=400, unit=UnitSymbol.VOLTAGE
multiplier=0, value=200, unit=UnitSymbol.VOLTAGE
),
)

Expand Down Expand Up @@ -168,7 +194,7 @@ async def get_energy_transfer_mode(
self, protocol: Protocol
) -> EnergyTransferModeEnum:
"""Overrides EVControllerInterface.get_energy_transfer_mode()."""
return self.config.energy_transfer_mode
return EnergyTransferModeEnum(EVEREST_EV_STATE.EnergyTransferMode)

async def get_supported_energy_services(self) -> List[ServiceV20]:
"""Overrides EVControllerInterface.get_energy_transfer_service()."""
Expand Down Expand Up @@ -444,7 +470,13 @@ async def process_dynamic_se_params(

async def is_cert_install_needed(self) -> bool:
"""Overrides EVControllerInterface.is_cert_install_needed()."""
return self.config.is_cert_install_needed

contract_leaf_path = os.path.join(get_PKI_PATH(), CertPath.CONTRACT_LEAF_DER)

if self.config.is_cert_install_needed or os.path.exists(contract_leaf_path) is False:
return True
else:
return False

async def process_sa_schedules_dinspec(
self, sa_schedules: List[SAScheduleTupleEntryDINSPEC]
Expand Down Expand Up @@ -525,23 +557,45 @@ async def process_sa_schedules_v2(

async def continue_charging(self) -> bool:
"""Overrides EVControllerInterface.continue_charging()."""
if self.charging_loop_cycles == 10 or await self.is_charging_complete():
# To simulate a bit of a charging loop, we'll let it run 10 times
return False
else:
self.charging_loop_cycles += 1
# The line below can just be called once process_message in all states
# are converted to async calls
# await asyncio.sleep(0.5)
return True
return not EVEREST_EV_STATE.StopCharging

async def reset_ev_values(self):
EVEREST_EV_STATE.reset()


async def store_contract_cert_and_priv_key(
self, contract_cert: bytes, priv_key: bytes
self, contract_cert_chain: CertificateChain, priv_key: bytes
):
"""Overrides EVControllerInterface.store_contract_cert_and_priv_key()."""
# TODO Need to store the contract cert and private key
pass

priv_key_pwd = "123456"
ec_priv_key = to_ec_priv_key(priv_key)
serialized_priv_key = ec_priv_key.private_bytes(
encoding=Encoding.PEM,
format=PrivateFormat.PKCS8,
encryption_algorithm=BestAvailableEncryption(priv_key_pwd.encode(encoding="utf-8"))
)
with open(os.path.join(get_PKI_PATH(), KeyPath.CONTRACT_LEAF_PEM), 'wb') as f:
f.write(serialized_priv_key)
with open(os.path.join(get_PKI_PATH(), KeyPasswordPath.CONTRACT_LEAF_KEY_PASSWORD), 'wb') as f:
f.write(priv_key_pwd.encode(encoding="utf-8"))
with open(os.path.join(get_PKI_PATH(), CertPath.CONTRACT_LEAF_DER), 'wb') as f:
f.write(contract_cert_chain.certificate)

for sub_ca_cert in contract_cert_chain.sub_certificates.certificates:
cert = load_der_x509_certificate(sub_ca_cert)
path_len = cert.extensions.get_extension_for_oid(
ExtensionOID.BASIC_CONSTRAINTS
).value.path_length
if path_len == 0:
with open(os.path.join(get_PKI_PATH(), CertPath.MO_SUB_CA2_DER), 'wb') as f:
f.write(sub_ca_cert)
elif path_len == 1:
with open(os.path.join(get_PKI_PATH(), CertPath.MO_SUB_CA1_DER), 'wb') as f:
f.write(sub_ca_cert)
else:
raise CertChainLengthError(allowed_num_sub_cas=2, num_sub_cas=path_len)

async def get_prioritised_emaids(self) -> Optional[EMAIDList]:
return None

Expand Down
3 changes: 3 additions & 0 deletions iso15118/evcc/everest/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .context import Context

context = Context()
22 changes: 22 additions & 0 deletions iso15118/evcc/everest/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
from typing import Callable

from .ev_state import EVState

PublisherCallback = Callable[[str, any], None]

class Context:
def __init__(self):
self._es = EVState()
self._pub_callback: PublisherCallback = None

def set_publish_callback(self, callback: PublisherCallback):
self._pub_callback = callback

def publish(self, variable_name: str, value: any):
self._pub_callback(variable_name, value)

@property
def ev_state(self) -> EVState:
return self._es
13 changes: 13 additions & 0 deletions iso15118/evcc/everest/ev_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
from dataclasses import dataclass, field

@dataclass
class EVState:
# Common
PaymentOption: str = ''
EnergyTransferMode: str = ''
StopCharging = False

def reset(self):
self.StopCharging = False
10 changes: 10 additions & 0 deletions iso15118/evcc/states/din_spec_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@

logger = logging.getLogger(__name__)

# *** EVerest code start ***
from iso15118.evcc.everest import context as EVEREST_CTX
# *** EVerest code end ***

# ============================================================================
# | EVCC STATES- DIN SPEC 70121 |
Expand Down Expand Up @@ -383,6 +386,10 @@ async def process_message(
charge_parameter_discovery_res.sa_schedule_list.values
)

# EVerest code start #
EVEREST_CTX.publish('AC_EVPowerReady', True)
# EVerest code end #

cable_check_req = CableCheckReq(
dc_ev_status=await ev_controller.get_dc_ev_status_dinspec(),
)
Expand Down Expand Up @@ -567,6 +574,9 @@ async def process_message(
):
self.comm_session.ongoing_timer = -1
power_delivery_req: PowerDeliveryReq = await self.build_power_delivery_req()

EVEREST_CTX.publish('DC_PowerOn', None)

self.create_next_message(
PowerDelivery,
power_delivery_req,
Expand Down
32 changes: 30 additions & 2 deletions iso15118/evcc/states/iso15118_2_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
from iso15118.evcc import evcc_settings
from iso15118.evcc.comm_session_handler import EVCCCommunicationSession
from iso15118.evcc.states.evcc_state import StateEVCC
from iso15118.shared.exceptions import DecryptionError, PrivateKeyReadError
from iso15118.shared.exceptions import (
DecryptionError,
PrivateKeyReadError,
CertChainLengthError,
)
from iso15118.shared.exi_codec import EXI
from iso15118.shared.messages.app_protocol import (
SupportedAppProtocolReq,
Expand Down Expand Up @@ -107,6 +111,9 @@

logger = logging.getLogger(__name__)

# *** EVerest code start ***
from iso15118.evcc.everest import context as EVEREST_CTX
# *** EVerest code end ***

# ============================================================================
# | COMMON EVCC STATES (FOR BOTH AC AND DC CHARGING) - ISO 15118-2 |
Expand Down Expand Up @@ -567,7 +574,7 @@ async def process_message(
)

await self.comm_session.ev_controller.store_contract_cert_and_priv_key(
cert_install_res.contract_cert_chain.certificate, decrypted_priv_key
cert_install_res.contract_cert_chain, decrypted_priv_key
)
except DecryptionError:
self.stop_state_machine(
Expand All @@ -581,6 +588,13 @@ async def process_message(
f"CertificateInstallationRes. {exc}"
)
return
except CertChainLengthError:
self.stop_state_machine(
f"CertChainLengthError, max "
f"{exc.allowed_num_sub_cas} sub-CAs allowed "
f"but {exc.num_sub_cas} sub-CAs provided"
)
return

payment_details_req = PaymentDetailsReq(
emaid=get_cert_cn(load_cert(os.path.join(get_PKI_PATH(), CertPath.CONTRACT_LEAF_DER))),
Expand Down Expand Up @@ -774,6 +788,10 @@ async def process_message(
charge_params_res.sa_schedule_list.schedule_tuples
)

# EVerest code start #
EVEREST_CTX.publish('AC_EVPowerReady', True)
# EVerest code end #

if self.comm_session.selected_charging_type_is_ac:

power_delivery_req = PowerDeliveryReq(
Expand Down Expand Up @@ -1118,6 +1136,12 @@ async def process_message(
charging_status_res: ChargingStatusRes = msg.body.charging_status_res
ac_evse_status: ACEVSEStatus = charging_status_res.ac_evse_status

# EVerest code start #
if charging_status_res.evse_max_current:
evse_max_current = charging_status_res.evse_max_current.value * pow(10, charging_status_res.evse_max_current.multiplier)
EVEREST_CTX.publish('AC_EVSEMaxCurrent', evse_max_current)
# EVerest code end #

if charging_status_res.receipt_required and self.comm_session.is_tls:
logger.debug("SECC requested MeteringReceipt")

Expand Down Expand Up @@ -1172,6 +1196,7 @@ async def process_message(
)
logger.debug(f"ChargeProgress is set to {ChargeProgress.RENEGOTIATE}")
elif ac_evse_status.evse_notification == EVSENotification.STOP_CHARGING:
EVEREST_CTX.publish('AC_StopFromCharger', None)
await self.stop_charging()

elif await self.comm_session.ev_controller.continue_charging():
Expand Down Expand Up @@ -1319,6 +1344,9 @@ async def process_message(
await ev_controller.get_dc_ev_power_delivery_parameter()
),
)

EVEREST_CTX.publish('DC_PowerOn', None)

self.create_next_message(
PowerDelivery,
power_delivery_req,
Expand Down
2 changes: 2 additions & 0 deletions iso15118/evcc/states/sap_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ async def process_message(
)
raise MessageProcessingError("SupportedAppProtocolReq")
break

await self.comm_session.ev_controller.reset_ev_values()

if match:
logger.info(f"Chosen protocol: {self.comm_session.protocol}")
Expand Down
5 changes: 5 additions & 0 deletions iso15118/shared/comm_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ async def rcv_loop(self, timeout: float):
str(self.current_state)
)
if self.current_state.next_v2gtp_msg:
if self.comm_session.__class__.__name__ == "EVCCCommunicationSession":
if self.current_state.message.__str__() == "CurrentDemandReq" or self.current_state.message.__str__() == "CurrentDemandRes":
await asyncio.sleep(0.25)
else:
await asyncio.sleep(0.5)
# next_v2gtp_msg would not be set only if the next state is either
# Terminate or Pause on the EVCC side
await self.send(self.current_state.next_v2gtp_msg)
Expand Down
Loading