Merge dependabot/npm_and_yarn/types/node-20.8.4 into combined-prs-branch
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
1 import { promiseWithTimeout } from './Utils';
2 import {
3 ProcedureName,
4 type ProtocolResponse,
5 type RequestPayload,
6 type ResponsePayload,
7 ResponseStatus,
8 } from '@/types';
9 import config from '@/assets/config';
10
11 type ResponseHandler = {
12 procedureName: ProcedureName;
13 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void;
14 reject: (reason?: unknown) => void;
15 };
16
17 export class UIClient {
18 private static instance: UIClient | null = null;
19
20 private ws!: WebSocket;
21 private responseHandlers: Map<string, ResponseHandler>;
22
23 private constructor() {
24 this.openWS();
25 this.responseHandlers = new Map<string, ResponseHandler>();
26 }
27
28 public static getInstance() {
29 if (UIClient.instance === null) {
30 UIClient.instance = new UIClient();
31 }
32 return UIClient.instance;
33 }
34
35 public registerWSonOpenListener(listener: (event: Event) => void) {
36 this.ws.addEventListener('open', listener);
37 }
38
39 public async startSimulator(): Promise<ResponsePayload> {
40 return this.sendRequest(ProcedureName.START_SIMULATOR, {});
41 }
42
43 public async stopSimulator(): Promise<ResponsePayload> {
44 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {});
45 }
46
47 public async listChargingStations(): Promise<ResponsePayload> {
48 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {});
49 }
50
51 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
52 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] });
53 }
54
55 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
56 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] });
57 }
58
59 public async openConnection(hashId: string): Promise<ResponsePayload> {
60 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
61 hashIds: [hashId],
62 });
63 }
64
65 public async closeConnection(hashId: string): Promise<ResponsePayload> {
66 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
67 hashIds: [hashId],
68 });
69 }
70
71 public async startTransaction(
72 hashId: string,
73 connectorId: number,
74 idTag: string | undefined,
75 ): Promise<ResponsePayload> {
76 return this.sendRequest(ProcedureName.START_TRANSACTION, {
77 hashIds: [hashId],
78 connectorId,
79 idTag,
80 });
81 }
82
83 public async stopTransaction(
84 hashId: string,
85 transactionId: number | undefined,
86 ): Promise<ResponsePayload> {
87 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
88 hashIds: [hashId],
89 transactionId,
90 });
91 }
92
93 public async startAutomaticTransactionGenerator(
94 hashId: string,
95 connectorId: number,
96 ): Promise<ResponsePayload> {
97 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
98 hashIds: [hashId],
99 connectorIds: [connectorId],
100 });
101 }
102
103 public async stopAutomaticTransactionGenerator(
104 hashId: string,
105 connectorId: number,
106 ): Promise<ResponsePayload> {
107 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
108 hashIds: [hashId],
109 connectorIds: [connectorId],
110 });
111 }
112
113 private openWS(): void {
114 this.ws = new WebSocket(
115 `ws://${config.uiServer.host}:${config.uiServer.port}`,
116 config.uiServer.protocol,
117 );
118 this.ws.onmessage = this.responseHandler.bind(this);
119 this.ws.onerror = (errorEvent) => {
120 console.error('WebSocket error: ', errorEvent);
121 };
122 this.ws.onclose = (closeEvent) => {
123 console.info('WebSocket closed: ', closeEvent);
124 };
125 }
126
127 private setResponseHandler(
128 id: string,
129 procedureName: ProcedureName,
130 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void,
131 reject: (reason?: unknown) => void,
132 ): void {
133 this.responseHandlers.set(id, { procedureName, resolve, reject });
134 }
135
136 private getResponseHandler(id: string): ResponseHandler | undefined {
137 return this.responseHandlers.get(id);
138 }
139
140 private deleteResponseHandler(id: string): boolean {
141 return this.responseHandlers.delete(id);
142 }
143
144 private async sendRequest(
145 command: ProcedureName,
146 data: RequestPayload,
147 ): Promise<ResponsePayload> {
148 let uuid: string;
149 return promiseWithTimeout(
150 new Promise<ResponsePayload>((resolve, reject) => {
151 uuid = crypto.randomUUID();
152 const msg = JSON.stringify([uuid, command, data]);
153
154 if (this.ws.readyState !== WebSocket.OPEN) {
155 this.openWS();
156 }
157 if (this.ws.readyState === WebSocket.OPEN) {
158 this.ws.send(msg);
159 } else {
160 throw new Error(`Send request '${command}' message: connection not opened`);
161 }
162
163 this.setResponseHandler(uuid, command, resolve, reject);
164 }),
165 120 * 1000,
166 Error(`Send request '${command}' message timeout`),
167 () => {
168 this.responseHandlers.delete(uuid);
169 },
170 );
171 }
172
173 private responseHandler(messageEvent: MessageEvent<string>): void {
174 const response = JSON.parse(messageEvent.data) as ProtocolResponse;
175
176 if (Array.isArray(response) === false) {
177 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`);
178 }
179
180 const [uuid, responsePayload] = response;
181
182 if (this.responseHandlers.has(uuid) === true) {
183 switch (responsePayload.status) {
184 case ResponseStatus.SUCCESS:
185 this.getResponseHandler(uuid)?.resolve(responsePayload);
186 break;
187 case ResponseStatus.FAILURE:
188 this.getResponseHandler(uuid)?.reject(responsePayload);
189 break;
190 default:
191 console.error(`Response status not supported: ${responsePayload.status}`);
192 }
193 this.deleteResponseHandler(uuid);
194 } else {
195 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`);
196 }
197 }
198 }