Commit | Line | Data |
---|---|---|
b794f42e | 1 | import argparse |
fa16d389 S |
2 | import asyncio |
3 | import logging | |
fa16d389 | 4 | from datetime import datetime, timezone |
aea49501 | 5 | from threading import Timer |
fa16d389 | 6 | |
a89844d4 | 7 | import ocpp.v201 |
1a0d2c47 | 8 | import websockets |
fa16d389 | 9 | from ocpp.routing import on |
d4aa9700 JB |
10 | from ocpp.v201.enums import ( |
11 | Action, | |
12 | AuthorizationStatusType, | |
13 | ClearCacheStatusType, | |
e3861e41 | 14 | GenericDeviceModelStatusType, |
d4aa9700 | 15 | RegistrationStatusType, |
f937c172 | 16 | ReportBaseType, |
b794f42e | 17 | TransactionEventType, |
d4aa9700 | 18 | ) |
a5c2d21f | 19 | from websockets import ConnectionClosed |
fa16d389 S |
20 | |
21 | # Setting up the logging configuration to display debug level messages. | |
22 | logging.basicConfig(level=logging.DEBUG) | |
23 | ||
a5c2d21f JB |
24 | ChargePoints = set() |
25 | ||
1a0d2c47 | 26 | |
aea49501 | 27 | class RepeatTimer(Timer): |
d4aa9700 | 28 | """Class that inherits from the Timer class. It will run a |
aea49501 JB |
29 | function at regular intervals.""" |
30 | ||
31 | def run(self): | |
32 | while not self.finished.wait(self.interval): | |
33 | self.function(*self.args, **self.kwargs) | |
34 | ||
35 | ||
fa16d389 | 36 | # Define a ChargePoint class inheriting from the OCPP 2.0.1 ChargePoint class. |
a89844d4 | 37 | class ChargePoint(ocpp.v201.ChargePoint): |
4de4645d JB |
38 | _command_timer: RepeatTimer |
39 | ||
aea49501 | 40 | # Message handlers to receive OCPP messages. |
339f65ad | 41 | @on(Action.BootNotification) |
d6488e8d | 42 | async def on_boot_notification(self, charging_station, reason, **kwargs): |
7628b7e6 | 43 | logging.info("Received %s", Action.BootNotification) |
d6488e8d | 44 | # Create and return a BootNotification response with the current time, |
5dd22b9f | 45 | # an interval of 60 seconds, and an accepted status. |
339f65ad | 46 | return ocpp.v201.call_result.BootNotification( |
d6488e8d | 47 | current_time=datetime.now(timezone.utc).isoformat(), |
115f3b17 | 48 | interval=60, |
d4aa9700 | 49 | status=RegistrationStatusType.accepted, |
d6488e8d | 50 | ) |
1a0d2c47 | 51 | |
115f3b17 | 52 | @on(Action.Heartbeat) |
5dd22b9f | 53 | async def on_heartbeat(self, **kwargs): |
7628b7e6 | 54 | logging.info("Received %s", Action.Heartbeat) |
d4aa9700 JB |
55 | return ocpp.v201.call_result.Heartbeat( |
56 | current_time=datetime.now(timezone.utc).isoformat() | |
57 | ) | |
115f3b17 | 58 | |
65c0600c | 59 | @on(Action.StatusNotification) |
d4aa9700 JB |
60 | async def on_status_notification( |
61 | self, timestamp, evse_id: int, connector_id: int, connector_status, **kwargs | |
62 | ): | |
7628b7e6 | 63 | logging.info("Received %s", Action.StatusNotification) |
65c0600c JB |
64 | return ocpp.v201.call_result.StatusNotification() |
65 | ||
5dd22b9f JB |
66 | @on(Action.Authorize) |
67 | async def on_authorize(self, id_token, **kwargs): | |
7628b7e6 | 68 | logging.info("Received %s", Action.Authorize) |
5dd22b9f | 69 | return ocpp.v201.call_result.Authorize( |
d4aa9700 | 70 | id_token_info={"status": AuthorizationStatusType.accepted} |
8430af0a | 71 | ) |
5dd22b9f | 72 | |
22c4f1fc | 73 | @on(Action.TransactionEvent) |
d4aa9700 JB |
74 | async def on_transaction_event( |
75 | self, | |
76 | event_type: TransactionEventType, | |
77 | timestamp, | |
78 | trigger_reason, | |
79 | seq_no: int, | |
80 | transaction_info, | |
81 | **kwargs, | |
82 | ): | |
22c4f1fc JB |
83 | match event_type: |
84 | case TransactionEventType.started: | |
7628b7e6 | 85 | logging.info("Received %s Started", Action.TransactionEvent) |
22c4f1fc | 86 | return ocpp.v201.call_result.TransactionEvent( |
d4aa9700 | 87 | id_token_info={"status": AuthorizationStatusType.accepted} |
8430af0a | 88 | ) |
22c4f1fc | 89 | case TransactionEventType.updated: |
7628b7e6 | 90 | logging.info("Received %s Updated", Action.TransactionEvent) |
d4aa9700 | 91 | return ocpp.v201.call_result.TransactionEvent(total_cost=10) |
22c4f1fc | 92 | case TransactionEventType.ended: |
7628b7e6 | 93 | logging.info("Received %s Ended", Action.TransactionEvent) |
22c4f1fc JB |
94 | return ocpp.v201.call_result.TransactionEvent() |
95 | ||
5dd22b9f | 96 | @on(Action.MeterValues) |
c7f80bf9 | 97 | async def on_meter_values(self, evse_id: int, meter_value, **kwargs): |
7628b7e6 | 98 | logging.info("Received %s", Action.MeterValues) |
5dd22b9f JB |
99 | return ocpp.v201.call_result.MeterValues() |
100 | ||
f937c172 | 101 | @on(Action.GetBaseReport) |
1f71f83f | 102 | async def on_get_base_report( |
103 | self, request_id: int, report_base: ReportBaseType, **kwargs | |
104 | ): | |
7c945b4a | 105 | logging.info("Received %s", Action.GetBaseReport) |
b2254601 | 106 | return ocpp.v201.call_result.GetBaseReport( |
e3861e41 | 107 | status=GenericDeviceModelStatusType.accepted |
1f71f83f | 108 | ) |
f937c172 | 109 | |
d6488e8d | 110 | # Request handlers to emit OCPP messages. |
4de4645d | 111 | async def _send_clear_cache(self): |
a89844d4 JB |
112 | request = ocpp.v201.call.ClearCache() |
113 | response = await self.call(request) | |
114 | ||
115 | if response.status == ClearCacheStatusType.accepted: | |
7628b7e6 | 116 | logging.info("%s successful", Action.ClearCache) |
a89844d4 | 117 | else: |
7628b7e6 | 118 | logging.info("%s failed", Action.ClearCache) |
1a0d2c47 | 119 | |
4de4645d | 120 | async def _send_get_base_report(self): |
b794f42e | 121 | request = ocpp.v201.call.GetBaseReport( |
e3861e41 | 122 | request_id=1, report_base=ReportBaseType.full_inventory |
299eb3fa S |
123 | ) |
124 | response = await self.call(request) | |
b794f42e | 125 | |
e3861e41 | 126 | if response.status == GenericDeviceModelStatusType.accepted: |
299eb3fa S |
127 | logging.info("%s successful", Action.GetBaseReport) |
128 | else: | |
891ae31d | 129 | logging.info("%s failed", Action.GetBaseReport) |
b2254601 | 130 | |
4de4645d | 131 | async def _send_command(self, command_name: Action): |
e3861e41 | 132 | logging.debug("Sending OCPP command: %s", command_name) |
299eb3fa S |
133 | match command_name: |
134 | case Action.ClearCache: | |
4de4645d | 135 | await self._send_clear_cache() |
299eb3fa | 136 | case Action.GetBaseReport: |
4de4645d | 137 | await self._send_get_base_report() |
e3861e41 JB |
138 | case _: |
139 | logging.info(f"Not supported command {command_name}") | |
299eb3fa | 140 | |
4de4645d JB |
141 | async def send_command(self, command_name: Action, delay=None, period=None): |
142 | if not delay and not period: | |
143 | raise ValueError("Either delay or period must be set") | |
144 | try: | |
145 | if delay and delay > 0: | |
146 | await asyncio.sleep(delay) | |
147 | await self._send_command(command_name) | |
148 | if period and period > 0 and not self._command_timer: | |
149 | self._command_timer = RepeatTimer( | |
150 | period, | |
151 | self._send_command, | |
152 | [command_name], | |
153 | ) | |
154 | self._command_timer.start() | |
155 | except ConnectionClosed: | |
156 | self.handle_connection_closed() | |
157 | ||
158 | def handle_connection_closed(self): | |
159 | logging.info("ChargePoint %s closed connection", self.id) | |
160 | if self._command_timer: | |
161 | self._command_timer.cancel() | |
162 | ChargePoints.remove(self) | |
e3861e41 | 163 | logging.debug("Connected ChargePoint(s): %d", len(ChargePoints)) |
f937c172 | 164 | |
fa16d389 S |
165 | |
166 | # Function to handle new WebSocket connections. | |
299eb3fa | 167 | async def on_connect(websocket, path): |
d4aa9700 | 168 | """For every new charge point that connects, create a ChargePoint instance and start |
d6488e8d JB |
169 | listening for messages.""" |
170 | try: | |
d4aa9700 | 171 | requested_protocols = websocket.request_headers["Sec-WebSocket-Protocol"] |
d6488e8d JB |
172 | except KeyError: |
173 | logging.info("Client hasn't requested any Subprotocol. Closing Connection") | |
174 | return await websocket.close() | |
1a0d2c47 | 175 | |
d6488e8d JB |
176 | if websocket.subprotocol: |
177 | logging.info("Protocols Matched: %s", websocket.subprotocol) | |
178 | else: | |
d4aa9700 JB |
179 | logging.warning( |
180 | "Protocols Mismatched | Expected Subprotocols: %s," | |
181 | " but client supports %s | Closing connection", | |
182 | websocket.available_subprotocols, | |
183 | requested_protocols, | |
184 | ) | |
d6488e8d | 185 | return await websocket.close() |
1a0d2c47 | 186 | |
d4aa9700 | 187 | charge_point_id = path.strip("/") |
d6488e8d | 188 | cp = ChargePoint(charge_point_id, websocket) |
1a0d2c47 | 189 | |
a5c2d21f | 190 | ChargePoints.add(cp) |
7628b7e6 JB |
191 | try: |
192 | await cp.start() | |
193 | except ConnectionClosed: | |
4de4645d | 194 | cp.handle_connection_closed() |
1a0d2c47 | 195 | |
b738a0fc | 196 | |
fa16d389 S |
197 | # Main function to start the WebSocket server. |
198 | async def main(): | |
b2254601 | 199 | # Define argument parser |
299eb3fa S |
200 | parser = argparse.ArgumentParser(description="OCPP2 Charge Point Simulator") |
201 | parser.add_argument("--command", type=str, help="OCPP2 Command Name") | |
b2254601 S |
202 | parser.add_argument("--delay", type=int, help="Delay in seconds") |
203 | parser.add_argument("--period", type=int, help="Period in seconds") | |
204 | ||
d6488e8d JB |
205 | # Create the WebSocket server and specify the handler for new connections. |
206 | server = await websockets.serve( | |
299eb3fa | 207 | on_connect, |
d4aa9700 | 208 | "127.0.0.1", # Listen on loopback. |
d6488e8d | 209 | 9000, # Port number. |
d4aa9700 | 210 | subprotocols=["ocpp2.0", "ocpp2.0.1"], # Specify OCPP 2.0.1 subprotocols. |
d6488e8d JB |
211 | ) |
212 | logging.info("WebSocket Server Started") | |
f937c172 | 213 | |
299eb3fa S |
214 | args = parser.parse_args() |
215 | ||
216 | if args.command: | |
217 | for cp in ChargePoints: | |
e3861e41 | 218 | await asyncio.create_task( |
4de4645d | 219 | cp.send_command(cp, args.command, args.delay, args.period) |
b738a0fc | 220 | ) |
299eb3fa | 221 | |
d6488e8d JB |
222 | # Wait for the server to close (runs indefinitely). |
223 | await server.wait_closed() | |
1a0d2c47 | 224 | |
fa16d389 S |
225 | |
226 | # Entry point of the script. | |
d4aa9700 | 227 | if __name__ == "__main__": |
d6488e8d JB |
228 | # Run the main function to start the server. |
229 | asyncio.run(main()) |