build(deps-dev): apply updates
[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):
a5c2d21f
JB
35 def __init__(self, charge_point_id, connection):
36 super().__init__(self, charge_point_id, connection)
37 self._ws_ping_timer = RepeatTimer(60, self.web_socket_ping)
38 self._ws_ping_timer.start()
39
40 def stop(self):
41 self._ws_ping_timer.cancel()
42
43 def web_socket_ping(self):
44 try:
45 self._connection.ping()
46 except ConnectionClosed:
47 ChargePoints.remove(self)
48 self.stop()
49
aea49501 50 # Message handlers to receive OCPP messages.
339f65ad 51 @on(Action.BootNotification)
d6488e8d
JB
52 async def on_boot_notification(self, charging_station, reason, **kwargs):
53 logging.info("Received BootNotification")
54 # Create and return a BootNotification response with the current time,
5dd22b9f 55 # an interval of 60 seconds, and an accepted status.
339f65ad 56 return ocpp.v201.call_result.BootNotification(
d6488e8d 57 current_time=datetime.now(timezone.utc).isoformat(),
115f3b17 58 interval=60,
d4aa9700 59 status=RegistrationStatusType.accepted,
d6488e8d 60 )
1a0d2c47 61
115f3b17 62 @on(Action.Heartbeat)
5dd22b9f 63 async def on_heartbeat(self, **kwargs):
115f3b17 64 logging.info("Received Heartbeat")
d4aa9700
JB
65 return ocpp.v201.call_result.Heartbeat(
66 current_time=datetime.now(timezone.utc).isoformat()
67 )
115f3b17 68
65c0600c 69 @on(Action.StatusNotification)
d4aa9700
JB
70 async def on_status_notification(
71 self, timestamp, evse_id: int, connector_id: int, connector_status, **kwargs
72 ):
65c0600c
JB
73 logging.info("Received StatusNotification")
74 return ocpp.v201.call_result.StatusNotification()
75
5dd22b9f
JB
76 @on(Action.Authorize)
77 async def on_authorize(self, id_token, **kwargs):
78 logging.info("Received Authorize")
79 return ocpp.v201.call_result.Authorize(
d4aa9700 80 id_token_info={"status": AuthorizationStatusType.accepted}
8430af0a 81 )
5dd22b9f 82
22c4f1fc 83 @on(Action.TransactionEvent)
d4aa9700
JB
84 async def on_transaction_event(
85 self,
86 event_type: TransactionEventType,
87 timestamp,
88 trigger_reason,
89 seq_no: int,
90 transaction_info,
91 **kwargs,
92 ):
22c4f1fc
JB
93 match event_type:
94 case TransactionEventType.started:
95 logging.info("Received TransactionEvent Started")
96 return ocpp.v201.call_result.TransactionEvent(
d4aa9700 97 id_token_info={"status": AuthorizationStatusType.accepted}
8430af0a 98 )
22c4f1fc
JB
99 case TransactionEventType.updated:
100 logging.info("Received TransactionEvent Updated")
d4aa9700 101 return ocpp.v201.call_result.TransactionEvent(total_cost=10)
22c4f1fc
JB
102 case TransactionEventType.ended:
103 logging.info("Received TransactionEvent Ended")
104 return ocpp.v201.call_result.TransactionEvent()
105
5dd22b9f 106 @on(Action.MeterValues)
c7f80bf9 107 async def on_meter_values(self, evse_id: int, meter_value, **kwargs):
5dd22b9f
JB
108 logging.info("Received MeterValues")
109 return ocpp.v201.call_result.MeterValues()
110
d6488e8d 111 # Request handlers to emit OCPP messages.
a89844d4
JB
112 async def send_clear_cache(self):
113 request = ocpp.v201.call.ClearCache()
114 response = await self.call(request)
115
116 if response.status == ClearCacheStatusType.accepted:
e1c2dac9 117 logging.info("Cache clearing successful")
a89844d4
JB
118 else:
119 logging.info("Cache clearing failed")
1a0d2c47 120
fa16d389
S
121
122# Function to handle new WebSocket connections.
123async def on_connect(websocket, path):
d4aa9700 124 """For every new charge point that connects, create a ChargePoint instance and start
d6488e8d
JB
125 listening for messages."""
126 try:
d4aa9700 127 requested_protocols = websocket.request_headers["Sec-WebSocket-Protocol"]
d6488e8d
JB
128 except KeyError:
129 logging.info("Client hasn't requested any Subprotocol. Closing Connection")
130 return await websocket.close()
1a0d2c47 131
d6488e8d
JB
132 if websocket.subprotocol:
133 logging.info("Protocols Matched: %s", websocket.subprotocol)
134 else:
d4aa9700
JB
135 logging.warning(
136 "Protocols Mismatched | Expected Subprotocols: %s,"
137 " but client supports %s | Closing connection",
138 websocket.available_subprotocols,
139 requested_protocols,
140 )
d6488e8d 141 return await websocket.close()
1a0d2c47 142
d4aa9700 143 charge_point_id = path.strip("/")
d6488e8d 144 cp = ChargePoint(charge_point_id, websocket)
a5c2d21f 145 ChargePoints.add(cp)
1a0d2c47 146
d6488e8d
JB
147 # Start the ChargePoint instance to listen for incoming messages.
148 await cp.start()
1a0d2c47 149
fa16d389
S
150
151# Main function to start the WebSocket server.
152async def main():
d6488e8d
JB
153 # Create the WebSocket server and specify the handler for new connections.
154 server = await websockets.serve(
155 on_connect,
d4aa9700 156 "127.0.0.1", # Listen on loopback.
d6488e8d 157 9000, # Port number.
d4aa9700 158 subprotocols=["ocpp2.0", "ocpp2.0.1"], # Specify OCPP 2.0.1 subprotocols.
d6488e8d
JB
159 )
160 logging.info("WebSocket Server Started")
161 # Wait for the server to close (runs indefinitely).
162 await server.wait_closed()
1a0d2c47 163
fa16d389
S
164
165# Entry point of the script.
d4aa9700 166if __name__ == "__main__":
d6488e8d
JB
167 # Run the main function to start the server.
168 asyncio.run(main())