fix(ui): reject request promise if the UI server response is malformed
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
1 import {
2 ApplicationProtocol,
3 AuthenticationType,
4 ProcedureName,
5 type ProtocolResponse,
6 type RequestPayload,
7 type ResponsePayload,
8 ResponseStatus,
9 type UIServerConfigurationSection
10 } from '@/types'
11
12 type ResponseHandler = {
13 procedureName: ProcedureName
14 resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void
15 reject: (reason?: unknown) => void
16 }
17
18 export class UIClient {
19 private static readonly instances: Map<number, UIClient> = new Map<number, UIClient>()
20
21 private ws!: WebSocket
22 private responseHandlers: Map<string, ResponseHandler>
23
24 private constructor(private uiServerConfiguration: UIServerConfigurationSection) {
25 this.openWS()
26 this.responseHandlers = new Map<string, ResponseHandler>()
27 }
28
29 public static getInstance(
30 serverId: number,
31 uiServerConfiguration?: UIServerConfigurationSection
32 ): UIClient {
33 if (!UIClient.instances.has(serverId) && uiServerConfiguration != null) {
34 UIClient.instances.set(serverId, new UIClient(uiServerConfiguration))
35 } else if (!UIClient.instances.has(serverId)) {
36 throw new Error(`UI client instance not found for server id: ${serverId}`)
37 }
38 return UIClient.instances.get(serverId)!
39 }
40
41 public registerWSEventListener<K extends keyof WebSocketEventMap>(
42 event: K,
43 listener: (event: WebSocketEventMap[K]) => void
44 ) {
45 this.ws.addEventListener(event, listener)
46 }
47
48 public async startSimulator(): Promise<ResponsePayload> {
49 return this.sendRequest(ProcedureName.START_SIMULATOR, {})
50 }
51
52 public async stopSimulator(): Promise<ResponsePayload> {
53 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
54 }
55
56 public async listTemplates(): Promise<ResponsePayload> {
57 return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
58 }
59
60 public async listChargingStations(): Promise<ResponsePayload> {
61 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
62 }
63
64 public async addChargingStations(
65 template: string,
66 numberOfStations: number
67 ): Promise<ResponsePayload> {
68 return this.sendRequest(ProcedureName.ADD_CHARGING_STATIONS, { template, numberOfStations })
69 }
70
71 public async deleteChargingStation(hashId: string): Promise<ResponsePayload> {
72 return this.sendRequest(ProcedureName.DELETE_CHARGING_STATIONS, { hashIds: [hashId] })
73 }
74
75 public async setSupervisionUrl(hashId: string, supervisionUrl: string): Promise<ResponsePayload> {
76 return this.sendRequest(ProcedureName.SET_SUPERVISION_URL, {
77 hashIds: [hashId],
78 url: supervisionUrl
79 })
80 }
81
82 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
83 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
84 }
85
86 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
87 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
88 }
89
90 public async openConnection(hashId: string): Promise<ResponsePayload> {
91 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
92 hashIds: [hashId]
93 })
94 }
95
96 public async closeConnection(hashId: string): Promise<ResponsePayload> {
97 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
98 hashIds: [hashId]
99 })
100 }
101
102 public async startTransaction(
103 hashId: string,
104 connectorId: number,
105 idTag: string | undefined
106 ): Promise<ResponsePayload> {
107 return this.sendRequest(ProcedureName.START_TRANSACTION, {
108 hashIds: [hashId],
109 connectorId,
110 idTag
111 })
112 }
113
114 public async stopTransaction(
115 hashId: string,
116 transactionId: number | undefined
117 ): Promise<ResponsePayload> {
118 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
119 hashIds: [hashId],
120 transactionId
121 })
122 }
123
124 public async startAutomaticTransactionGenerator(
125 hashId: string,
126 connectorId: number
127 ): Promise<ResponsePayload> {
128 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
129 hashIds: [hashId],
130 connectorIds: [connectorId]
131 })
132 }
133
134 public async stopAutomaticTransactionGenerator(
135 hashId: string,
136 connectorId: number
137 ): Promise<ResponsePayload> {
138 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
139 hashIds: [hashId],
140 connectorIds: [connectorId]
141 })
142 }
143
144 private openWS(): void {
145 const protocols =
146 this.uiServerConfiguration.authentication?.enabled === true &&
147 this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
148 ? [
149 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
150 `authorization.basic.${btoa(`${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}`).replace(/={1,2}$/, '')}`
151 ]
152 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
153 this.ws = new WebSocket(
154 `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
155 protocols
156 )
157 this.ws.onopen = openEvent => {
158 console.info('WebSocket opened', openEvent)
159 }
160 this.ws.onmessage = this.responseHandler.bind(this)
161 this.ws.onerror = errorEvent => {
162 console.error('WebSocket error: ', errorEvent)
163 }
164 this.ws.onclose = closeEvent => {
165 console.info('WebSocket closed: ', closeEvent)
166 }
167 }
168
169 private async sendRequest(
170 procedureName: ProcedureName,
171 payload: RequestPayload
172 ): Promise<ResponsePayload> {
173 return new Promise<ResponsePayload>((resolve, reject) => {
174 if (this.ws.readyState === WebSocket.OPEN) {
175 const uuid = crypto.randomUUID()
176 const msg = JSON.stringify([uuid, procedureName, payload])
177 const sendTimeout = setTimeout(() => {
178 this.responseHandlers.delete(uuid)
179 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
180 }, 60000)
181 try {
182 this.ws.send(msg)
183 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
184 } catch (error) {
185 this.responseHandlers.delete(uuid)
186 reject(error)
187 } finally {
188 clearTimeout(sendTimeout)
189 }
190 } else {
191 reject(new Error(`Send request '${procedureName}' message: connection closed`))
192 }
193 })
194 }
195
196 private responseHandler(messageEvent: MessageEvent<string>): void {
197 const response = JSON.parse(messageEvent.data) as ProtocolResponse
198
199 if (Array.isArray(response) === false) {
200 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
201 }
202
203 const [uuid, responsePayload] = response
204
205 if (this.responseHandlers.has(uuid) === true) {
206 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
207 switch (responsePayload.status) {
208 case ResponseStatus.SUCCESS:
209 resolve(responsePayload)
210 break
211 case ResponseStatus.FAILURE:
212 reject(responsePayload)
213 break
214 default:
215 reject(
216 new Error(
217 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
218 )
219 )
220 }
221 this.responseHandlers.delete(uuid)
222 } else {
223 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
224 }
225 }
226 }