fix(ocpp-server): properly handle charge point disconnection
[e-mobility-charging-stations-simulator.git] / tests / ocpp-server / server.py
CommitLineData
fa16d389
S
1import asyncio
2import logging
fa16d389 3from datetime import datetime, timezone
aea49501 4from threading import Timer
fa16d389 5
a89844d4 6import ocpp.v201
1a0d2c47 7import websockets
fa16d389 8from ocpp.routing import on
d4aa9700
JB
9from ocpp.v201.enums import (
10 Action,
11 AuthorizationStatusType,
12 ClearCacheStatusType,
13 RegistrationStatusType,
14 TransactionEventType,
15)
a5c2d21f 16from websockets import ConnectionClosed
fa16d389
S
17
18# Setting up the logging configuration to display debug level messages.
19logging.basicConfig(level=logging.DEBUG)
20
a5c2d21f
JB
21ChargePoints = set()
22
1a0d2c47 23
aea49501 24class RepeatTimer(Timer):
d4aa9700 25 """Class that inherits from the Timer class. It will run a
aea49501
JB
26 function at regular intervals."""
27
28 def run(self):
29 while not self.finished.wait(self.interval):
30 self.function(*self.args, **self.kwargs)
31
32
fa16d389 33# Define a ChargePoint class inheriting from the OCPP 2.0.1 ChargePoint class.
a89844d4 34class ChargePoint(ocpp.v201.ChargePoint):
aea49501 35 # Message handlers to receive OCPP messages.
339f65ad 36 @on(Action.BootNotification)
d6488e8d 37 async def on_boot_notification(self, charging_station, reason, **kwargs):
7628b7e6 38 logging.info("Received %s", Action.BootNotification)
d6488e8d 39 # Create and return a BootNotification response with the current time,
5dd22b9f 40 # an interval of 60 seconds, and an accepted status.
339f65ad 41 return ocpp.v201.call_result.BootNotification(
d6488e8d 42 current_time=datetime.now(timezone.utc).isoformat(),
115f3b17 43 interval=60,
d4aa9700 44 status=RegistrationStatusType.accepted,
d6488e8d 45 )
1a0d2c47 46
115f3b17 47 @on(Action.Heartbeat)
5dd22b9f 48 async def on_heartbeat(self, **kwargs):
7628b7e6 49 logging.info("Received %s", Action.Heartbeat)
d4aa9700
JB
50 return ocpp.v201.call_result.Heartbeat(
51 current_time=datetime.now(timezone.utc).isoformat()
52 )
115f3b17 53
65c0600c 54 @on(Action.StatusNotification)
d4aa9700
JB
55 async def on_status_notification(
56 self, timestamp, evse_id: int, connector_id: int, connector_status, **kwargs
57 ):
7628b7e6 58 logging.info("Received %s", Action.StatusNotification)
65c0600c
JB
59 return ocpp.v201.call_result.StatusNotification()
60
5dd22b9f
JB
61 @on(Action.Authorize)
62 async def on_authorize(self, id_token, **kwargs):
7628b7e6 63 logging.info("Received %s", Action.Authorize)
5dd22b9f 64 return ocpp.v201.call_result.Authorize(
d4aa9700 65 id_token_info={"status": AuthorizationStatusType.accepted}
8430af0a 66 )
5dd22b9f 67
22c4f1fc 68 @on(Action.TransactionEvent)
d4aa9700
JB
69 async def on_transaction_event(
70 self,
71 event_type: TransactionEventType,
72 timestamp,
73 trigger_reason,
74 seq_no: int,
75 transaction_info,
76 **kwargs,
77 ):
22c4f1fc
JB
78 match event_type:
79 case TransactionEventType.started:
7628b7e6 80 logging.info("Received %s Started", Action.TransactionEvent)
22c4f1fc 81 return ocpp.v201.call_result.TransactionEvent(
d4aa9700 82 id_token_info={"status": AuthorizationStatusType.accepted}
8430af0a 83 )
22c4f1fc 84 case TransactionEventType.updated:
7628b7e6 85 logging.info("Received %s Updated", Action.TransactionEvent)
d4aa9700 86 return ocpp.v201.call_result.TransactionEvent(total_cost=10)
22c4f1fc 87 case TransactionEventType.ended:
7628b7e6 88 logging.info("Received %s Ended", Action.TransactionEvent)
22c4f1fc
JB
89 return ocpp.v201.call_result.TransactionEvent()
90
5dd22b9f 91 @on(Action.MeterValues)
c7f80bf9 92 async def on_meter_values(self, evse_id: int, meter_value, **kwargs):
7628b7e6 93 logging.info("Received %s", Action.MeterValues)
5dd22b9f
JB
94 return ocpp.v201.call_result.MeterValues()
95
d6488e8d 96 # Request handlers to emit OCPP messages.
a89844d4
JB
97 async def send_clear_cache(self):
98 request = ocpp.v201.call.ClearCache()
99 response = await self.call(request)
100
101 if response.status == ClearCacheStatusType.accepted:
7628b7e6 102 logging.info("%s successful", Action.ClearCache)
a89844d4 103 else:
7628b7e6 104 logging.info("%s failed", Action.ClearCache)
1a0d2c47 105
fa16d389
S
106
107# Function to handle new WebSocket connections.
108async def on_connect(websocket, path):
d4aa9700 109 """For every new charge point that connects, create a ChargePoint instance and start
d6488e8d
JB
110 listening for messages."""
111 try:
d4aa9700 112 requested_protocols = websocket.request_headers["Sec-WebSocket-Protocol"]
d6488e8d
JB
113 except KeyError:
114 logging.info("Client hasn't requested any Subprotocol. Closing Connection")
115 return await websocket.close()
1a0d2c47 116
d6488e8d
JB
117 if websocket.subprotocol:
118 logging.info("Protocols Matched: %s", websocket.subprotocol)
119 else:
d4aa9700
JB
120 logging.warning(
121 "Protocols Mismatched | Expected Subprotocols: %s,"
122 " but client supports %s | Closing connection",
123 websocket.available_subprotocols,
124 requested_protocols,
125 )
d6488e8d 126 return await websocket.close()
1a0d2c47 127
d4aa9700 128 charge_point_id = path.strip("/")
d6488e8d 129 cp = ChargePoint(charge_point_id, websocket)
a5c2d21f 130 ChargePoints.add(cp)
7628b7e6
JB
131 try:
132 await cp.start()
133 except ConnectionClosed:
134 logging.info("ChargePoint %s closed connection", cp.id)
135 ChargePoints.remove(cp)
136 logging.debug("Connected charge points: %d", len(ChargePoints))
1a0d2c47 137
fa16d389
S
138
139# Main function to start the WebSocket server.
140async def main():
d6488e8d
JB
141 # Create the WebSocket server and specify the handler for new connections.
142 server = await websockets.serve(
143 on_connect,
d4aa9700 144 "127.0.0.1", # Listen on loopback.
d6488e8d 145 9000, # Port number.
d4aa9700 146 subprotocols=["ocpp2.0", "ocpp2.0.1"], # Specify OCPP 2.0.1 subprotocols.
d6488e8d
JB
147 )
148 logging.info("WebSocket Server Started")
149 # Wait for the server to close (runs indefinitely).
150 await server.wait_closed()
1a0d2c47 151
fa16d389
S
152
153# Entry point of the script.
d4aa9700 154if __name__ == "__main__":
d6488e8d
JB
155 # Run the main function to start the server.
156 asyncio.run(main())