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