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