Skip to content

Commit 003ccf5

Browse files
authored
OCPP (#1893)
* OCPP * fixes * fix mac * logging * flake8 * fix test * clean up imports * typo, json config
1 parent 980a9fe commit 003ccf5

14 files changed

Lines changed: 374 additions & 89 deletions

File tree

packages/control/chargepoint/chargepoint.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,15 @@ def is_charging_possible(self) -> Tuple[bool, Optional[str]]:
206206
def _process_charge_stop(self) -> None:
207207
# Charging Ev ist noch das EV des vorherigen Zyklus, wenn das nicht -1 war und jetzt nicht mehr geladen
208208
# werden soll (-1), Daten zurücksetzen.
209+
# Ocpp Stop Funktion aufrufen
210+
if not self.data.get.plug_state and self.data.set.ocpp_transaction_id is not None:
211+
data.data.optional_data.stop_transaction(
212+
self.data.config.ocpp_chargebox_id,
213+
self.chargepoint_module.fault_state,
214+
self.data.get.imported,
215+
self.data.set.ocpp_transaction_id,
216+
self.data.set.rfid)
217+
Pub().pub("openWB/set/chargepoint/"+str(self.num)+"/set/ocpp_transaction_id", None)
209218
if self.data.set.charging_ev_prev != -1:
210219
# Daten zurücksetzen, wenn nicht geladen werden soll.
211220
self.reset_control_parameter_at_charge_stop()
@@ -712,6 +721,17 @@ def update(self, ev_list: Dict[str, Ev]) -> None:
712721
self._pub_connected_vehicle(ev_list[f"ev{vehicle}"])
713722
else:
714723
self._pub_configured_ev(ev_list)
724+
# OCPP Start Transaction nach Anstecken
725+
if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or
726+
(self.data.set.ocpp_transaction_id is None and self.data.get.charge_state)):
727+
self.data.set.ocpp_transaction_id = data.data.optional_data.start_transaction(
728+
self.data.config.ocpp_chargebox_id,
729+
self.chargepoint_module.fault_state,
730+
self.num,
731+
self.data.set.rfid or self.data.get.rfid or self.data.get.vehicle_id,
732+
self.data.get.imported)
733+
Pub().pub("openWB/set/chargepoint/"+str(self.num) +
734+
"/set/ocpp_transaction_id", self.data.set.ocpp_transaction_id)
715735
# SoC nach Anstecken aktualisieren
716736
if ((self.data.get.plug_state and self.data.set.plug_state_prev is False) or
717737
(self.data.get.plug_state is False and self.data.set.plug_state_prev)):

packages/control/chargepoint/chargepoint_data.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ class Set:
140140
rfid: Optional[str] = None
141141
target_current: float = 0 # Soll-Strom aus fest vorgegebener Stromstärke
142142
charging_ev_data: Ev = field(default_factory=ev_factory)
143+
ocpp_transaction_id: Optional[int] = None
143144

144145

145146
@dataclass
@@ -154,6 +155,7 @@ class Config:
154155
auto_phase_switch_hw: bool = False
155156
control_pilot_interruption_hw: bool = False
156157
id: int = 0
158+
ocpp_chargebox_id: Optional[str] = None
157159

158160
def __post_init__(self):
159161
self.event_update_state: threading.Event

packages/control/ocpp.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
from datetime import datetime
2+
import json
3+
import logging
4+
from ocpp.v16 import call, ChargePoint as OcppChargepoint
5+
import websockets
6+
import asyncio
7+
from typing import Callable, Optional
8+
9+
from control import data
10+
from control.optional_data import OptionalProtocol
11+
from modules.common.fault_state import FaultState
12+
13+
14+
log = logging.getLogger(__name__)
15+
16+
17+
class OcppMixin:
18+
def _get_formatted_time(self: OptionalProtocol) -> str:
19+
return datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
20+
21+
def _process_call(self: OptionalProtocol,
22+
chargebox_id: str,
23+
fault_state: FaultState,
24+
func: Callable) -> Optional[websockets.WebSocketClientProtocol]:
25+
async def make_call() -> websockets.WebSocketClientProtocol:
26+
async with websockets.connect(self.data.ocpp.url+chargebox_id,
27+
subprotocols=[self.data.ocpp.version]) as ws:
28+
try:
29+
cp = OcppChargepoint(chargebox_id, ws, 2)
30+
await cp.call(func)
31+
except asyncio.exceptions.TimeoutError:
32+
# log.exception("Erwarteter TimeOut StartTransaction")
33+
pass
34+
return ws
35+
try:
36+
if self.data.ocpp.active and chargebox_id:
37+
return asyncio.run(make_call())
38+
except websockets.exceptions.InvalidStatusCode:
39+
fault_state.warning(f"Chargebox ID {chargebox_id} konnte nicht im OCPP-Backend gefunden werden oder "
40+
"URL des Backends ist falsch.")
41+
return None
42+
43+
def boot_notification(self: OptionalProtocol,
44+
chargebox_id: str,
45+
fault_state: FaultState,
46+
model: str,
47+
serial_number: str) -> Optional[int]:
48+
try:
49+
self._process_call(chargebox_id, fault_state, call.BootNotification(
50+
charge_point_model=model,
51+
charge_point_vendor="openWB",
52+
firmware_version=data.data.system_data["system"].data["version"],
53+
meter_serial_number=serial_number
54+
))
55+
except Exception as e:
56+
fault_state.from_exception(e)
57+
58+
def start_transaction(self: OptionalProtocol,
59+
chargebox_id: str,
60+
fault_state: FaultState,
61+
connector_id: int,
62+
id_tag: str,
63+
imported: int) -> Optional[int]:
64+
try:
65+
ws = self._process_call(chargebox_id, fault_state, call.StartTransaction(
66+
connector_id=connector_id,
67+
id_tag=id_tag if id_tag else "",
68+
meter_start=int(imported),
69+
timestamp=self._get_formatted_time()
70+
))
71+
if ws:
72+
tansaction_id = json.loads(ws.messages[0])[2]["transactionId"]
73+
log.debug(f"Transaction ID: {tansaction_id} für Chargebox ID: {chargebox_id} mit Tag: {id_tag} und "
74+
f"Zählerstand: {imported} erhalten.")
75+
return tansaction_id
76+
except Exception as e:
77+
fault_state.from_exception(e)
78+
return None
79+
80+
def transfer_values(self: OptionalProtocol,
81+
chargebox_id: str,
82+
fault_state: FaultState,
83+
connector_id: int,
84+
imported: int) -> None:
85+
try:
86+
self._process_call(chargebox_id, fault_state, call.MeterValues(
87+
connector_id=connector_id,
88+
meter_value=[{"timestamp": self._get_formatted_time(),
89+
"sampledValue": [
90+
{
91+
"value": f'{int(imported)}',
92+
"context": "Sample.Periodic",
93+
"format": "Raw",
94+
"measurand": "Energy.Active.Import.Register",
95+
"unit": "Wh"
96+
},
97+
]}],
98+
))
99+
log.debug(f"Zählerstand {imported} an Chargebox ID: {chargebox_id} übermittelt.")
100+
except Exception as e:
101+
fault_state.from_exception(e)
102+
103+
def send_heart_beat(self: OptionalProtocol, chargebox_id: str, fault_state: FaultState) -> None:
104+
try:
105+
self._process_call(chargebox_id, fault_state, call.Heartbeat())
106+
log.debug(f"Heartbeat an Chargebox ID: {chargebox_id} gesendet.")
107+
except Exception as e:
108+
fault_state.from_exception(e)
109+
110+
def stop_transaction(self: OptionalProtocol,
111+
chargebox_id: str,
112+
fault_state: FaultState,
113+
imported: int,
114+
transaction_id: int,
115+
id_tag: str) -> None:
116+
try:
117+
self._process_call(chargebox_id, fault_state, call.StopTransaction(meter_stop=int(imported),
118+
timestamp=self._get_formatted_time(),
119+
transaction_id=transaction_id,
120+
reason="EVDisconnected",
121+
id_tag=id_tag if id_tag else ""
122+
))
123+
log.debug(f"Transaction mit ID: {transaction_id} für Chargebox ID: {chargebox_id} mit Tag: {id_tag} und "
124+
f"Zählerstand: {imported} beendet.")
125+
except Exception as e:
126+
fault_state.from_exception(e)

packages/control/ocpp_test.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from unittest.mock import Mock
2+
import pytest
3+
4+
from control import data
5+
from control.chargepoint.chargepoint import Chargepoint
6+
from control.chargepoint.chargepoint_template import CpTemplate
7+
from control.counter import Counter
8+
from control.ev import Ev
9+
from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule
10+
from modules.chargepoints.mqtt.config import Mqtt
11+
12+
13+
@pytest.fixture()
14+
def mock_data() -> None:
15+
data.data_init(Mock())
16+
data.data.optional_data.data.ocpp.active = True
17+
data.data.optional_data.data.ocpp.url = "ws://localhost:9000/"
18+
19+
20+
def test_start_transaction(mock_data, monkeypatch):
21+
cp = Chargepoint(1, None)
22+
cp.data.config.ocpp_chargebox_id = "cp1"
23+
cp.data.get.plug_state = True
24+
cp.template = CpTemplate()
25+
cp.chargepoint_module = ChargepointModule(Mqtt())
26+
27+
start_transaction_mock = Mock()
28+
monkeypatch.setattr(data.data.optional_data, "start_transaction", start_transaction_mock)
29+
_pub_configured_ev_mock = Mock()
30+
monkeypatch.setattr(cp, "_pub_configured_ev", _pub_configured_ev_mock)
31+
32+
cp.update([])
33+
34+
assert start_transaction_mock.call_args == (("cp1", cp.chargepoint_module.fault_state, 1, None, 0),)
35+
36+
37+
def test_stop_transaction(mock_data, monkeypatch):
38+
cp = Chargepoint(1, None)
39+
cp.data.config.ocpp_chargebox_id = "cp1"
40+
cp.data.get.plug_state = False
41+
cp.data.set.ocpp_transaction_id = 124
42+
cp.data.set.charging_ev_prev = 1
43+
cp.chargepoint_module = ChargepointModule(Mqtt())
44+
cp.template = CpTemplate()
45+
46+
stop_transaction_mock = Mock()
47+
monkeypatch.setattr(data.data.optional_data, "stop_transaction", stop_transaction_mock)
48+
get_evu_counter_mock = Mock(return_value=Mock(spec=Counter))
49+
monkeypatch.setattr(data.data.counter_all_data, "get_evu_counter", get_evu_counter_mock)
50+
data.data.ev_data["ev1"] = Ev(1)
51+
52+
cp._process_charge_stop()
53+
54+
assert stop_transaction_mock.call_args == (("cp1", cp.chargepoint_module.fault_state, 0, 124, None),)
55+
56+
57+
def test_send_ocpp_data(mock_data, monkeypatch):
58+
data.data.cp_data["cp1"] = Chargepoint(1, None)
59+
data.data.cp_data["cp1"].data.config.ocpp_chargebox_id = "cp1"
60+
data.data.cp_data["cp1"].data.get.plug_state = True
61+
data.data.cp_data["cp1"].chargepoint_module = ChargepointModule(Mqtt())
62+
data.data.cp_data["cp1"].data.get.serial_number = "123456"
63+
transfer_values_mock = Mock()
64+
monkeypatch.setattr(data.data.optional_data, "transfer_values", transfer_values_mock)
65+
boot_notification_mock = Mock()
66+
monkeypatch.setattr(data.data.optional_data, "boot_notification", boot_notification_mock)
67+
send_heart_beat_mock = Mock()
68+
monkeypatch.setattr(data.data.optional_data, "send_heart_beat", send_heart_beat_mock)
69+
70+
data.data.optional_data.ocpp_boot_notification_sent = False
71+
72+
data.data.optional_data._transfer_meter_values()
73+
74+
boot_notification_mock.call_args == (("cp1", "mqtt", "123456"),)
75+
send_heart_beat_mock.call_args == (("cp1",),)
76+
transfer_values_mock.call_args == (("cp1", 1, 0),)
77+
assert data.data.optional_data.ocpp_boot_notification_sent is True

packages/control/optional.py

Lines changed: 30 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,31 @@
11
"""Optionale Module
22
"""
3-
from dataclasses import dataclass, field
43
import logging
54
from math import ceil # Aufrunden
65
import threading
7-
from typing import Dict, List
6+
from typing import List
87

9-
from dataclass_utils.factories import empty_dict_factory
8+
from control import data
9+
from control.ocpp import OcppMixin
10+
from control.optional_data import OptionalData
1011
from helpermodules import hardware_configuration
1112
from helpermodules.constants import NO_ERROR
1213
from helpermodules.pub import Pub
1314
from helpermodules.timecheck import create_unix_timestamp_current_full_hour
1415
from helpermodules.utils import thread_handler
1516
from modules.common.configurable_tariff import ConfigurableElectricityTariff
16-
from modules.display_themes.cards.config import CardsDisplayTheme
1717

1818
log = logging.getLogger(__name__)
1919

2020

21-
@dataclass
22-
class EtGet:
23-
fault_state: int = 0
24-
fault_str: str = NO_ERROR
25-
prices: Dict = field(default_factory=empty_dict_factory)
26-
27-
28-
def get_factory() -> EtGet:
29-
return EtGet()
30-
31-
32-
@dataclass
33-
class Et:
34-
get: EtGet = field(default_factory=get_factory)
35-
36-
37-
def et_factory() -> Et:
38-
return Et()
39-
40-
41-
@dataclass
42-
class InternalDisplay:
43-
active: bool = False
44-
on_if_plugged_in: bool = True
45-
pin_active: bool = False
46-
pin_code: str = "0000"
47-
standby: int = 60
48-
theme: CardsDisplayTheme = CardsDisplayTheme()
49-
50-
51-
def int_display_factory() -> InternalDisplay:
52-
return InternalDisplay()
53-
54-
55-
@dataclass
56-
class Led:
57-
active: bool = False
58-
59-
60-
def led_factory() -> Led:
61-
return Led()
62-
63-
64-
@dataclass
65-
class Rfid:
66-
active: bool = False
67-
68-
69-
def rfid_factory() -> Rfid:
70-
return Rfid()
71-
72-
73-
@dataclass
74-
class OptionalData:
75-
et: Et = field(default_factory=et_factory)
76-
int_display: InternalDisplay = field(default_factory=int_display_factory)
77-
led: Led = field(default_factory=led_factory)
78-
rfid: Rfid = field(default_factory=rfid_factory)
79-
dc_charging: bool = False
80-
81-
82-
class Optional:
21+
class Optional(OcppMixin):
8322
def __init__(self):
8423
try:
8524
self.data = OptionalData()
8625
self.et_module: ConfigurableElectricityTariff = None
8726
self.data.dc_charging = hardware_configuration.get_hardware_configuration_setting("dc_charging")
8827
Pub().pub("openWB/optional/dc_charging", self.data.dc_charging)
28+
self.ocpp_boot_notification_sent = False
8929
except Exception:
9030
log.exception("Fehler im Optional-Modul")
9131

@@ -153,3 +93,27 @@ def et_get_prices(self):
15393
Pub().pub("openWB/set/optional/et/get/fault_str", NO_ERROR)
15494
except Exception:
15595
log.exception("Fehler im Optional-Modul")
96+
97+
def ocpp_transfer_meter_values(self):
98+
try:
99+
if self.data.ocpp.active:
100+
thread_handler(threading.Thread(target=self._transfer_meter_values, args=(), name="OCPP Client"))
101+
except Exception:
102+
log.exception("Fehler im OCPP-Optional-Modul")
103+
104+
def _transfer_meter_values(self):
105+
for cp in data.data.cp_data.values():
106+
try:
107+
if self.ocpp_boot_notification_sent is False:
108+
# Boot-Notfification nicht in der init-Funktion aufrufen, da noch nicht alles initialisiert ist
109+
self.boot_notification(cp.data.config.ocpp_chargebox_id,
110+
cp.chargepoint_module.fault_state,
111+
cp.chargepoint_module.config.type,
112+
cp.data.get.serial_number)
113+
self.ocpp_boot_notification_sent = True
114+
if cp.data.set.ocpp_transaction_id is not None:
115+
self.send_heart_beat(cp.data.config.ocpp_chargebox_id, cp.chargepoint_module.fault_state)
116+
self.transfer_values(cp.data.config.ocpp_chargebox_id,
117+
cp.chargepoint_module.fault_state, cp.num, int(cp.data.get.imported))
118+
except Exception:
119+
log.exception("Fehler im OCPP-Optional-Modul")

0 commit comments

Comments
 (0)