Skip to content
Merged

OCPP #1893

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
20 changes: 20 additions & 0 deletions packages/control/chargepoint/chargepoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,15 @@ def is_charging_possible(self) -> Tuple[bool, Optional[str]]:
def _process_charge_stop(self) -> None:
# Charging Ev ist noch das EV des vorherigen Zyklus, wenn das nicht -1 war und jetzt nicht mehr geladen
# werden soll (-1), Daten zurücksetzen.
# Ocpp Stop Funktion aufrufen
if not self.data.get.plug_state and self.data.set.ocpp_transaction_id is not None:
data.data.optional_data.stop_transaction(
self.data.config.ocpp_chargebox_id,
self.chargepoint_module.fault_state,
self.data.get.imported,
self.data.set.ocpp_transaction_id,
self.data.set.rfid)
Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/ocpp_transaction_id", None)
if self.data.set.charging_ev_prev != -1:
# Daten zurücksetzen, wenn nicht geladen werden soll.
self.reset_control_parameter_at_charge_stop()
Expand Down Expand Up @@ -712,6 +721,17 @@ def update(self, ev_list: Dict[str, Ev]) -> None:
self._pub_connected_vehicle(ev_list[f"ev{vehicle}"])
else:
self._pub_configured_ev(ev_list)
# OCPP Start Transaction nach Anstecken
if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or
(self.data.set.ocpp_transaction_id is None and self.data.get.charge_state)):
self.data.set.ocpp_transaction_id = data.data.optional_data.start_transaction(
self.data.config.ocpp_chargebox_id,
self.chargepoint_module.fault_state,
self.num,
self.data.set.rfid or self.data.get.rfid or self.data.get.vehicle_id,
self.data.get.imported)
Pub().pub("openWB/set/chargepoint/"+str(self.num) +
"/set/ocpp_transaction_id", self.data.set.ocpp_transaction_id)
# SoC nach Anstecken aktualisieren
if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or
(self.data.get.plug_state is False and self.data.set.plug_state_prev)):
Expand Down
2 changes: 2 additions & 0 deletions packages/control/chargepoint/chargepoint_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ class Set:
rfid: Optional[str] = None
target_current: float = 0 # Soll-Strom aus fest vorgegebener Stromstärke
charging_ev_data: Ev = field(default_factory=ev_factory)
ocpp_transaction_id: Optional[int] = None


@dataclass
Expand All @@ -154,6 +155,7 @@ class Config:
auto_phase_switch_hw: bool = False
control_pilot_interruption_hw: bool = False
id: int = 0
ocpp_chargebox_id: Optional[str] = None

def __post_init__(self):
self.event_update_state: threading.Event
Expand Down
126 changes: 126 additions & 0 deletions packages/control/ocpp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
from datetime import datetime
import json
import logging
from ocpp.v16 import call, ChargePoint as OcppChargepoint
import websockets
import asyncio
from typing import Callable, Optional

from control import data
from control.optional_data import OptionalProtocol
from modules.common.fault_state import FaultState


log = logging.getLogger(__name__)


class OcppMixin:
def _get_formatted_time(self: OptionalProtocol) -> str:
return datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")

def _process_call(self: OptionalProtocol,
chargebox_id: str,
fault_state: FaultState,
func: Callable) -> Optional[websockets.WebSocketClientProtocol]:
async def make_call() -> websockets.WebSocketClientProtocol:
async with websockets.connect(self.data.ocpp.url+chargebox_id,
subprotocols=[self.data.ocpp.version]) as ws:
try:
cp = OcppChargepoint(chargebox_id, ws, 2)
await cp.call(func)
except asyncio.exceptions.TimeoutError:
# log.exception("Erwarteter TimeOut StartTransaction")
pass
return ws
try:
if self.data.ocpp.active and chargebox_id:
return asyncio.run(make_call())
except websockets.exceptions.InvalidStatusCode:
fault_state.warning(f"Chargebox ID {chargebox_id} konnte nicht im OCPP-Backend gefunden werden oder "
"URL des Backends ist falsch.")
return None

def boot_notification(self: OptionalProtocol,
chargebox_id: str,
fault_state: FaultState,
model: str,
serial_number: str) -> Optional[int]:
try:
self._process_call(chargebox_id, fault_state, call.BootNotification(
charge_point_model=model,
charge_point_vendor="openWB",
firmware_version=data.data.system_data["system"].data["version"],
meter_serial_number=serial_number
))
except Exception as e:
fault_state.from_exception(e)

def start_transaction(self: OptionalProtocol,
chargebox_id: str,
fault_state: FaultState,
connector_id: int,
id_tag: str,
imported: int) -> Optional[int]:
try:
ws = self._process_call(chargebox_id, fault_state, call.StartTransaction(
connector_id=connector_id,
id_tag=id_tag if id_tag else "",
meter_start=int(imported),
timestamp=self._get_formatted_time()
))
if ws:
tansaction_id = json.loads(ws.messages[0])[2]["transactionId"]
log.debug(f"Transaction ID: {tansaction_id} für Chargebox ID: {chargebox_id} mit Tag: {id_tag} und "
f"Zählerstand: {imported} erhalten.")
return tansaction_id
except Exception as e:
fault_state.from_exception(e)
return None

def transfer_values(self: OptionalProtocol,
chargebox_id: str,
fault_state: FaultState,
connector_id: int,
imported: int) -> None:
try:
self._process_call(chargebox_id, fault_state, call.MeterValues(
connector_id=connector_id,
meter_value=[{"timestamp": self._get_formatted_time(),
"sampledValue": [
{
"value": f'{int(imported)}',
"context": "Sample.Periodic",
"format": "Raw",
"measurand": "Energy.Active.Import.Register",
"unit": "Wh"
},
]}],
))
log.debug(f"Zählerstand {imported} an Chargebox ID: {chargebox_id} übermittelt.")
except Exception as e:
fault_state.from_exception(e)

def send_heart_beat(self: OptionalProtocol, chargebox_id: str, fault_state: FaultState) -> None:
try:
self._process_call(chargebox_id, fault_state, call.Heartbeat())
log.debug(f"Heartbeat an Chargebox ID: {chargebox_id} gesendet.")
except Exception as e:
fault_state.from_exception(e)

def stop_transaction(self: OptionalProtocol,
chargebox_id: str,
fault_state: FaultState,
imported: int,
transaction_id: int,
id_tag: str) -> None:
try:
self._process_call(chargebox_id, fault_state, call.StopTransaction(meter_stop=int(imported),
timestamp=self._get_formatted_time(),
transaction_id=transaction_id,
reason="EVDisconnected",
id_tag=id_tag if id_tag else ""
))
log.debug(f"Transaction mit ID: {transaction_id} für Chargebox ID: {chargebox_id} mit Tag: {id_tag} und "
f"Zählerstand: {imported} beendet.")
except Exception as e:
fault_state.from_exception(e)
77 changes: 77 additions & 0 deletions packages/control/ocpp_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from unittest.mock import Mock
import pytest

from control import data
from control.chargepoint.chargepoint import Chargepoint
from control.chargepoint.chargepoint_template import CpTemplate
from control.counter import Counter
from control.ev import Ev
from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule
from modules.chargepoints.mqtt.config import Mqtt


@pytest.fixture()
def mock_data() -> None:
data.data_init(Mock())
data.data.optional_data.data.ocpp.active = True
data.data.optional_data.data.ocpp.url = "ws://localhost:9000/"


def test_start_transaction(mock_data, monkeypatch):
cp = Chargepoint(1, None)
cp.data.config.ocpp_chargebox_id = "cp1"
cp.data.get.plug_state = True
cp.template = CpTemplate()
cp.chargepoint_module = ChargepointModule(Mqtt())

start_transaction_mock = Mock()
monkeypatch.setattr(data.data.optional_data, "start_transaction", start_transaction_mock)
_pub_configured_ev_mock = Mock()
monkeypatch.setattr(cp, "_pub_configured_ev", _pub_configured_ev_mock)

cp.update([])

assert start_transaction_mock.call_args == (("cp1", cp.chargepoint_module.fault_state, 1, None, 0),)


def test_stop_transaction(mock_data, monkeypatch):
cp = Chargepoint(1, None)
cp.data.config.ocpp_chargebox_id = "cp1"
cp.data.get.plug_state = False
cp.data.set.ocpp_transaction_id = 124
cp.data.set.charging_ev_prev = 1
cp.chargepoint_module = ChargepointModule(Mqtt())
cp.template = CpTemplate()

stop_transaction_mock = Mock()
monkeypatch.setattr(data.data.optional_data, "stop_transaction", stop_transaction_mock)
get_evu_counter_mock = Mock(return_value=Mock(spec=Counter))
monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock)
data.data.ev_data["ev1"] = Ev(1)

cp._process_charge_stop()

assert stop_transaction_mock.call_args == (("cp1", cp.chargepoint_module.fault_state, 0, 124, None),)


def test_send_ocpp_data(mock_data, monkeypatch):
data.data.cp_data["cp1"] = Chargepoint(1, None)
data.data.cp_data["cp1"].data.config.ocpp_chargebox_id = "cp1"
data.data.cp_data["cp1"].data.get.plug_state = True
data.data.cp_data["cp1"].chargepoint_module = ChargepointModule(Mqtt())
data.data.cp_data["cp1"].data.get.serial_number = "123456"
transfer_values_mock = Mock()
monkeypatch.setattr(data.data.optional_data, "transfer_values", transfer_values_mock)
boot_notification_mock = Mock()
monkeypatch.setattr(data.data.optional_data, "boot_notification", boot_notification_mock)
send_heart_beat_mock = Mock()
monkeypatch.setattr(data.data.optional_data, "send_heart_beat", send_heart_beat_mock)

data.data.optional_data.ocpp_boot_notification_sent = False

data.data.optional_data._transfer_meter_values()

boot_notification_mock.call_args == (("cp1", "mqtt", "123456"),)
send_heart_beat_mock.call_args == (("cp1",),)
transfer_values_mock.call_args == (("cp1", 1, 0),)
assert data.data.optional_data.ocpp_boot_notification_sent is True
96 changes: 30 additions & 66 deletions packages/control/optional.py
Original file line number Diff line number Diff line change
@@ -1,91 +1,31 @@
"""Optionale Module
"""
from dataclasses import dataclass, field
import logging
from math import ceil # Aufrunden
import threading
from typing import Dict, List
from typing import List

from dataclass_utils.factories import empty_dict_factory
from control import data
from control.ocpp import OcppMixin
from control.optional_data import OptionalData
from helpermodules import hardware_configuration
from helpermodules.constants import NO_ERROR
from helpermodules.pub import Pub
from helpermodules.timecheck import create_unix_timestamp_current_full_hour
from helpermodules.utils import thread_handler
from modules.common.configurable_tariff import ConfigurableElectricityTariff
from modules.display_themes.cards.config import CardsDisplayTheme

log = logging.getLogger(__name__)


@dataclass
class EtGet:
fault_state: int = 0
fault_str: str = NO_ERROR
prices: Dict = field(default_factory=empty_dict_factory)


def get_factory() -> EtGet:
return EtGet()


@dataclass
class Et:
get: EtGet = field(default_factory=get_factory)


def et_factory() -> Et:
return Et()


@dataclass
class InternalDisplay:
active: bool = False
on_if_plugged_in: bool = True
pin_active: bool = False
pin_code: str = "0000"
standby: int = 60
theme: CardsDisplayTheme = CardsDisplayTheme()


def int_display_factory() -> InternalDisplay:
return InternalDisplay()


@dataclass
class Led:
active: bool = False


def led_factory() -> Led:
return Led()


@dataclass
class Rfid:
active: bool = False


def rfid_factory() -> Rfid:
return Rfid()


@dataclass
class OptionalData:
et: Et = field(default_factory=et_factory)
int_display: InternalDisplay = field(default_factory=int_display_factory)
led: Led = field(default_factory=led_factory)
rfid: Rfid = field(default_factory=rfid_factory)
dc_charging: bool = False


class Optional:
class Optional(OcppMixin):
def __init__(self):
try:
self.data = OptionalData()
self.et_module: ConfigurableElectricityTariff = None
self.data.dc_charging = hardware_configuration.get_hardware_configuration_setting("dc_charging")
Pub().pub("openWB/optional/dc_charging", self.data.dc_charging)
self.ocpp_boot_notification_sent = False
except Exception:
log.exception("Fehler im Optional-Modul")

Expand Down Expand Up @@ -153,3 +93,27 @@ def et_get_prices(self):
Pub().pub("openWB/set/optional/et/get/fault_str", NO_ERROR)
except Exception:
log.exception("Fehler im Optional-Modul")

def ocpp_transfer_meter_values(self):
try:
if self.data.ocpp.active:
thread_handler(threading.Thread(target=self._transfer_meter_values, args=(), name="OCPP Client"))
except Exception:
log.exception("Fehler im OCPP-Optional-Modul")

def _transfer_meter_values(self):
for cp in data.data.cp_data.values():
try:
if self.ocpp_boot_notification_sent is False:
# Boot-Notfification nicht in der init-Funktion aufrufen, da noch nicht alles initialisiert ist
self.boot_notification(cp.data.config.ocpp_chargebox_id,
cp.chargepoint_module.fault_state,
cp.chargepoint_module.config.type,
cp.data.get.serial_number)
self.ocpp_boot_notification_sent = True
if cp.data.set.ocpp_transaction_id is not None:
self.send_heart_beat(cp.data.config.ocpp_chargebox_id, cp.chargepoint_module.fault_state)
self.transfer_values(cp.data.config.ocpp_chargebox_id,
cp.chargepoint_module.fault_state, cp.num, int(cp.data.get.imported))
except Exception:
log.exception("Fehler im OCPP-Optional-Modul")
Loading