]> Piment Noir Git Repositories - e-mobility-charging-stations-simulator.git/blame - tests/ocpp-server/server.py
build(deps): bump the regular group across 1 directory with 6 updates (#1452)
[e-mobility-charging-stations-simulator.git] / tests / ocpp-server / server.py
CommitLineData
b794f42e 1import argparse
fa16d389
S
2import asyncio
3import logging
fa16d389 4from datetime import datetime, timezone
cbfbfbc1 5from functools import partial
8fe113d7 6from random import randint
cbfbfbc1 7from typing import Optional
fa16d389 8
a89844d4 9import ocpp.v201
1a0d2c47 10import websockets
fa16d389 11from ocpp.routing import on
d4aa9700
JB
12from ocpp.v201.enums import (
13 Action,
5dd0043f
JB
14 AuthorizationStatusEnumType,
15 ClearCacheStatusEnumType,
16 GenericDeviceModelStatusEnumType,
17 RegistrationStatusEnumType,
18 ReportBaseEnumType,
19 TransactionEventEnumType,
d4aa9700 20)
a5c2d21f 21from websockets import ConnectionClosed
fa16d389 22
cbfbfbc1
JB
23from timer import Timer
24
fa16d389
S
25# Setting up the logging configuration to display debug level messages.
26logging.basicConfig(level=logging.DEBUG)
27
a5c2d21f
JB
28ChargePoints = set()
29
1a0d2c47 30
a89844d4 31class ChargePoint(ocpp.v201.ChargePoint):
cbfbfbc1
JB
32 _command_timer: Optional[Timer]
33
34 def __init__(self, connection):
35 super().__init__(connection.path.strip("/"), connection)
36 self._command_timer = None
4de4645d 37
aea49501 38 # Message handlers to receive OCPP messages.
5dd0043f 39 @on(Action.boot_notification)
d6488e8d 40 async def on_boot_notification(self, charging_station, reason, **kwargs):
5dd0043f 41 logging.info("Received %s", Action.boot_notification)
d6488e8d 42 # Create and return a BootNotification response with the current time,
5dd22b9f 43 # an interval of 60 seconds, and an accepted status.
339f65ad 44 return ocpp.v201.call_result.BootNotification(
d6488e8d 45 current_time=datetime.now(timezone.utc).isoformat(),
115f3b17 46 interval=60,
5dd0043f 47 status=RegistrationStatusEnumType.accepted,
d6488e8d 48 )
1a0d2c47 49
5dd0043f 50 @on(Action.heartbeat)
5dd22b9f 51 async def on_heartbeat(self, **kwargs):
5dd0043f 52 logging.info("Received %s", Action.heartbeat)
d4aa9700
JB
53 return ocpp.v201.call_result.Heartbeat(
54 current_time=datetime.now(timezone.utc).isoformat()
55 )
115f3b17 56
5dd0043f 57 @on(Action.status_notification)
d4aa9700
JB
58 async def on_status_notification(
59 self, timestamp, evse_id: int, connector_id: int, connector_status, **kwargs
60 ):
5dd0043f 61 logging.info("Received %s", Action.status_notification)
65c0600c
JB
62 return ocpp.v201.call_result.StatusNotification()
63
5dd0043f 64 @on(Action.authorize)
5dd22b9f 65 async def on_authorize(self, id_token, **kwargs):
5dd0043f 66 logging.info("Received %s", Action.authorize)
5dd22b9f 67 return ocpp.v201.call_result.Authorize(
5dd0043f 68 id_token_info={"status": AuthorizationStatusEnumType.accepted}
8430af0a 69 )
5dd22b9f 70
5dd0043f 71 @on(Action.transaction_event)
d4aa9700
JB
72 async def on_transaction_event(
73 self,
5dd0043f 74 event_type: TransactionEventEnumType,
d4aa9700
JB
75 timestamp,
76 trigger_reason,
77 seq_no: int,
78 transaction_info,
79 **kwargs,
80 ):
22c4f1fc 81 match event_type:
5dd0043f
JB
82 case TransactionEventEnumType.started:
83 logging.info("Received %s Started", Action.transaction_event)
22c4f1fc 84 return ocpp.v201.call_result.TransactionEvent(
5dd0043f 85 id_token_info={"status": AuthorizationStatusEnumType.accepted}
8430af0a 86 )
5dd0043f
JB
87 case TransactionEventEnumType.updated:
88 logging.info("Received %s Updated", Action.transaction_event)
d4aa9700 89 return ocpp.v201.call_result.TransactionEvent(total_cost=10)
5dd0043f
JB
90 case TransactionEventEnumType.ended:
91 logging.info("Received %s Ended", Action.transaction_event)
22c4f1fc
JB
92 return ocpp.v201.call_result.TransactionEvent()
93
5dd0043f 94 @on(Action.meter_values)
c7f80bf9 95 async def on_meter_values(self, evse_id: int, meter_value, **kwargs):
5dd0043f 96 logging.info("Received %s", Action.meter_values)
5dd22b9f
JB
97 return ocpp.v201.call_result.MeterValues()
98
d6488e8d 99 # Request handlers to emit OCPP messages.
4de4645d 100 async def _send_clear_cache(self):
a89844d4
JB
101 request = ocpp.v201.call.ClearCache()
102 response = await self.call(request)
103
5dd0043f
JB
104 if response.status == ClearCacheStatusEnumType.accepted:
105 logging.info("%s successful", Action.clear_cache)
a89844d4 106 else:
5dd0043f 107 logging.info("%s failed", Action.clear_cache)
1a0d2c47 108
4de4645d 109 async def _send_get_base_report(self):
b794f42e 110 request = ocpp.v201.call.GetBaseReport(
8fe113d7 111 request_id=randint(1, 100), # noqa: S311
5dd0043f 112 report_base=ReportBaseEnumType.full_inventory,
299eb3fa
S
113 )
114 response = await self.call(request)
b794f42e 115
5dd0043f
JB
116 if response.status == GenericDeviceModelStatusEnumType.accepted:
117 logging.info("%s successful", Action.get_base_report)
299eb3fa 118 else:
5dd0043f 119 logging.info("%s failed", Action.get_base_report)
b2254601 120
4de4645d 121 async def _send_command(self, command_name: Action):
118332f4 122 logging.debug("Sending OCPP command %s", command_name)
299eb3fa 123 match command_name:
5dd0043f 124 case Action.clear_cache:
4de4645d 125 await self._send_clear_cache()
5dd0043f 126 case Action.get_base_report:
4de4645d 127 await self._send_get_base_report()
e3861e41
JB
128 case _:
129 logging.info(f"Not supported command {command_name}")
299eb3fa 130
cbfbfbc1
JB
131 async def send_command(
132 self, command_name: Action, delay: Optional[float], period: Optional[float]
133 ):
4de4645d 134 try:
cbfbfbc1
JB
135 if delay and not self._command_timer:
136 self._command_timer = Timer(
137 delay,
138 False,
139 self._send_command,
140 [command_name],
141 )
142 if period and not self._command_timer:
143 self._command_timer = Timer(
4de4645d 144 period,
cbfbfbc1 145 True,
4de4645d
JB
146 self._send_command,
147 [command_name],
148 )
4de4645d
JB
149 except ConnectionClosed:
150 self.handle_connection_closed()
151
152 def handle_connection_closed(self):
153 logging.info("ChargePoint %s closed connection", self.id)
154 if self._command_timer:
155 self._command_timer.cancel()
156 ChargePoints.remove(self)
e3861e41 157 logging.debug("Connected ChargePoint(s): %d", len(ChargePoints))
f937c172 158
fa16d389
S
159
160# Function to handle new WebSocket connections.
cbfbfbc1
JB
161async def on_connect(
162 websocket,
163 command_name: Optional[Action],
164 delay: Optional[float],
165 period: Optional[float],
166):
d4aa9700 167 """For every new charge point that connects, create a ChargePoint instance and start
ad8df5d3
JB
168 listening for messages.
169 """
d6488e8d 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
cbfbfbc1
JB
187 cp = ChargePoint(websocket)
188 if command_name:
189 await cp.send_command(command_name, delay, period)
1a0d2c47 190
a5c2d21f 191 ChargePoints.add(cp)
cbfbfbc1 192
7628b7e6
JB
193 try:
194 await cp.start()
195 except ConnectionClosed:
4de4645d 196 cp.handle_connection_closed()
1a0d2c47 197
b738a0fc 198
93d95199
JB
199def check_positive_number(value: Optional[float]):
200 try:
201 value = float(value)
202 except ValueError:
203 raise argparse.ArgumentTypeError("must be a number") from None
204 if value <= 0:
205 raise argparse.ArgumentTypeError("must be a positive number")
206 return value
207
208
fa16d389
S
209# Main function to start the WebSocket server.
210async def main():
cbfbfbc1 211 parser = argparse.ArgumentParser(description="OCPP2 Server")
ba56e7c9
JB
212 parser.add_argument("-c", "--command", type=Action, help="command name")
213 group = parser.add_mutually_exclusive_group()
93d95199
JB
214 group.add_argument(
215 "-d",
216 "--delay",
217 type=check_positive_number,
218 help="delay in seconds",
219 )
220 group.add_argument(
221 "-p",
222 "--period",
223 type=check_positive_number,
224 help="period in seconds",
225 )
ba56e7c9 226 group.required = parser.parse_known_args()[0].command is not None
cbfbfbc1
JB
227
228 args = parser.parse_args()
b2254601 229
d6488e8d
JB
230 # Create the WebSocket server and specify the handler for new connections.
231 server = await websockets.serve(
cbfbfbc1
JB
232 partial(
233 on_connect, command_name=args.command, delay=args.delay, period=args.period
234 ),
d4aa9700 235 "127.0.0.1", # Listen on loopback.
d6488e8d 236 9000, # Port number.
d4aa9700 237 subprotocols=["ocpp2.0", "ocpp2.0.1"], # Specify OCPP 2.0.1 subprotocols.
d6488e8d
JB
238 )
239 logging.info("WebSocket Server Started")
f937c172 240
d6488e8d
JB
241 # Wait for the server to close (runs indefinitely).
242 await server.wait_closed()
1a0d2c47 243
fa16d389
S
244
245# Entry point of the script.
d4aa9700 246if __name__ == "__main__":
d6488e8d
JB
247 # Run the main function to start the server.
248 asyncio.run(main())