Repository navigation
Expand file tree
/
Copy pathocpp-proxy.py
More file actions
237 lines (198 loc) · 8.9 KB
/
Copy pathocpp-proxy.py
File metadata and controls
237 lines (198 loc) · 8.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# Simple 1-1 OCPP proxy
import asyncio
import logging
import time
import websockets
import websockets.asyncio
import websockets.asyncio.server
import ssl
import argparse
import configparser
__version__ = "0.1.0"
config = configparser.ConfigParser()
logging.basicConfig(
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("proxy")
class OCPPProxy: # Forward declaration
pass
# main class
class OCPPProxy:
# Static dict of OCPPProxy instances. key is charger_id
proxy_list: dict[str, OCPPProxy] = {}
def __init__(self, websocket: websockets.asyncio.server.ServerConnection, charger_id: str):
# Store the websocket for later
logger.debug(websocket.request)
self.ws = websocket
self.charger_id = charger_id
# Check that charger id looks reasonable
if not charger_id.isalnum():
logger.error(f"Charger ID '{charger_id}' is not alphanumeric")
raise Exception("Charger ID is not alphanumeric")
# Insert new OCPPProxy instance in the (static) dict of instances.
self.proxy_list[charger_id] = self
async def close(self):
"""Close all connections to the charger and server"""
try:
await self.ws.close()
await self.server_connection.close()
except Exception as e:
pass # Ignore exceptions
@staticmethod
async def check_delete_old(charger_id: str):
"""Check if there are any old instances of this charger in the proxy list"""
if charger_id in OCPPProxy.proxy_list:
logger.info(f"Charger ID {charger_id} already exists. Closing and deleting")
proxy: OCPPProxy = OCPPProxy.proxy_list[charger_id]
await proxy.close()
del OCPPProxy.proxy_list[charger_id]
async def run(self):
"""Main loop for this proxy. This is where all the magic happens."""
# Create connection to the CSMS.
# Forward any available Authorization and User-Agent headers
headers = {}
if "Authorization" in self.ws.request.headers:
headers["Authorization"] = self.ws.request.headers["Authorization"]
logger.debug(f'Authorization header set to {headers["Authorization"]}')
user_agent = self.ws.request.headers.get("User-Agent", None)
subprotocols = self.ws.request.headers.get("Sec-WebSocket-Protocol", ["ocpp1.6"])
server_url = config.get("ext-server", "server") + "/" + self.charger_id
try:
self.server_connection = await websockets.connect(
uri=server_url,
user_agent_header=user_agent,
additional_headers=headers,
subprotocols=[subprotocols],
)
logger.info(f"{self.charger_id} Connected to server @ {server_url}")
# Create tasks to handle the charger. Each task handles receiving messages from
# the charger or the CSMS, and finally a watch dog task to take down
# connections if connection goes stale.
self._last_charger_update = time.time()
self.tasks = []
self.tasks.append(asyncio.create_task(self.receive_charger_messages()))
self.tasks.append(asyncio.create_task(self.receive_server_messages()))
#self.tasks.append(asyncio.create_task(self.watchdog()))
# Wait for tasks to complete
done, pending = await asyncio.wait(self.tasks, return_when=asyncio.FIRST_COMPLETED)
logger.debug(f"{self.charger_id} Task(s) completed: {done}, {pending}")
for task in done:
e = task.exception()
if e:
logger.warning(f"{self.charger_id} (Not serious - likely connection loss) Task {task} raised exception {e} related to charger ")
# Cancel any remaining tasks
for task in pending:
task.cancel()
except websockets.exceptions.InvalidURI:
logger.error(f"{self.charger_id} Invalid URI")
except websockets.exceptions.ConnectionClosedError as e:
logger.error(f"{self.charger_id} Connection closed unexpectedly: {e}")
except websockets.exceptions.InvalidHandshake:
logger.error(f"{self.charger_id} Handshake with the external server failed")
except Exception as e:
logger.error(f"{self.charger_id} Unexpected error: {e}")
finally:
# Always close stuff. close is well tempered, so can close even if not established
await self.close()
async def receive_charger_messages(self):
try:
while True:
# Wait for a message from the charger and forward it to the server
message = await self.ws.recv()
logger.info(f"{self.charger_id} ^ : {message}")
self._last_charger_update = time.time()
await self.server_connection.send(message)
except Exception as e:
logger.error(f"{self.charger_id} Error in receive_charger_messages: {e}")
async def receive_server_messages(self):
try:
while True:
# Wait for a message from the server and forward it to the charger
message = await self.server_connection.recv()
logger.info(f"{self.charger_id} v : {message}")
await self.ws.send(message)
except Exception as e:
logger.error(f"{self.charger_id} Error in receive_server_messages: {e}")
async def watchdog(self):
"""Watch time vs. timestamp updated by receiving messages from charger."""
while True:
# And ... sleep
await asyncio.sleep(config.getint("host", "watchdog_interval", 30))
elapsed = time.time() - self._last_charger_update
if elapsed > config.getint("host", "watchdog_stale", 300):
logger.error(f"{self.charger_id} Watch dog no for {elapsed} seconds. Closing connections")
return
# Connection handler (charger connects)
async def on_connect(websocket: websockets.asyncio.server.ServerConnection):
logger.debug('Connection request', websocket.request)
# Determine charger_id (final part of path)
path = websocket.request.path
charger_id = path.strip("/")
logger.info(f'{charger_id} connection request')
try:
# Delete any existing charger setup
if charger_id in OCPPProxy.proxy_list:
await OCPPProxy.proxy_list[charger_id].close()
del OCPPProxy.proxy_list[charger_id]
# Setup
proxy = OCPPProxy(websocket=websocket, charger_id=charger_id)
# Connect and run proxy operations
await proxy.run()
except Exception as e:
logger.error(f'{charger_id} Error creating OCPPProxy: {e}')
finally:
logger.info("f{charger_id} closed/done")
# Main. Decode arguments, setup handler
async def main():
parser = argparse.ArgumentParser(
description='ocpp-proxy: A simple 1-1 OCPP proxy')
parser.add_argument('--version', action='version',
version=f'%(prog)s {__version__}')
parser.add_argument(
"--config",
type=str,
default="ocpp-proxy.ini",
help="Configuration file (INI format). Default ocpp-proxy.ini",
)
args = parser.parse_args()
# Read config. config object is then available (via config import) to all.
logger.warning(f"Reading config from {args.config}")
config.read(args.config)
# Adjust log levels
for logger_name in config["logging"]:
logger.warning(f'Setting log level for {logger_name} to {config.get("logging", logger_name)}')
logging.getLogger(logger_name).setLevel(level=config.get("logging", logger_name))
# Get host config
host = config.get("host", "addr")
port = config.get("host", "port")
cert_chain = config.get("host", "cert_chain", fallback=None)
cert_key = config.get("host", "cert_key", fallback=None)
logger.debug(f"host: {host}, port: {port}, cert_chain: {cert_chain}, cert_key: {cert_key}")
# Start server, either ws:// or wss://
if cert_chain and cert_key:
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(certfile=cert_chain, keyfile=cert_key)
server = await websockets.serve(
on_connect,
host,
port,
subprotocols=["ocpp1.6", "ocpp2.0.1"],
ssl=ssl_context,
ping_timeout=config.getint("host", "ping_timeout"),
)
else:
server = await websockets.serve(
on_connect,
host,
port,
subprotocols=["ocpp1.6", "ocpp2.0.1"],
ping_timeout=config.getint("host", "ping_timeout"),
)
logger.info("Proxy ready. Waiting for new connections...")
await server.wait_closed()
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
exit(0)