refactor(ui): add vue.js error handler
[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
5a010bf0 48 public async listChargingStations(): Promise<ResponsePayload> {
66a7748d 49 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
32de5a57
LM
50 }
51
8fc2e5cc 52 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 53 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
54 }
55
56 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 57 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
58 }
59
60 public async openConnection(hashId: string): Promise<ResponsePayload> {
61 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
a974c8e4 62 hashIds: [hashId]
66a7748d 63 })
8fc2e5cc
JB
64 }
65
66 public async closeConnection(hashId: string): Promise<ResponsePayload> {
67 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
a974c8e4 68 hashIds: [hashId]
66a7748d 69 })
8fc2e5cc
JB
70 }
71
32de5a57
LM
72 public async startTransaction(
73 hashId: string,
74 connectorId: number,
66a7748d 75 idTag: string | undefined
32de5a57 76 ): Promise<ResponsePayload> {
32de5a57 77 return this.sendRequest(ProcedureName.START_TRANSACTION, {
757b2ecf 78 hashIds: [hashId],
32de5a57 79 connectorId,
a974c8e4 80 idTag
66a7748d 81 })
32de5a57
LM
82 }
83
5a010bf0
JB
84 public async stopTransaction(
85 hashId: string,
66a7748d 86 transactionId: number | undefined
5a010bf0 87 ): Promise<ResponsePayload> {
32de5a57 88 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
757b2ecf 89 hashIds: [hashId],
a974c8e4 90 transactionId
66a7748d 91 })
32de5a57
LM
92 }
93
757b2ecf
JB
94 public async startAutomaticTransactionGenerator(
95 hashId: string,
66a7748d 96 connectorId: number
757b2ecf
JB
97 ): Promise<ResponsePayload> {
98 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
99 hashIds: [hashId],
a974c8e4 100 connectorIds: [connectorId]
66a7748d 101 })
757b2ecf
JB
102 }
103
104 public async stopAutomaticTransactionGenerator(
105 hashId: string,
66a7748d 106 connectorId: number
757b2ecf
JB
107 ): Promise<ResponsePayload> {
108 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
109 hashIds: [hashId],
a974c8e4 110 connectorIds: [connectorId]
66a7748d 111 })
757b2ecf
JB
112 }
113
5a010bf0 114 private openWS(): void {
329eab0e
JB
115 const protocols =
116 this.configuration.uiServer.authentication?.enabled === true &&
117 this.configuration.uiServer.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
118 ? [
119 `${this.configuration.uiServer.protocol}${this.configuration.uiServer.version}`,
120 `authorization.basic.${btoa(`${this.configuration.uiServer.authentication.username}:${this.configuration.uiServer.authentication.password}`).replace(/={1,2}$/, '')}`
121 ]
122 : `${this.configuration.uiServer.protocol}${this.configuration.uiServer.version}`
12f26d4a 123 this.ws = new WebSocket(
9d76f5ec 124 `${this.configuration.uiServer.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.configuration.uiServer.host}:${this.configuration.uiServer.port}`,
329eab0e 125 protocols
66a7748d 126 )
e2372e58
JB
127 this.ws.onopen = openEvent => {
128 console.info('WebSocket opened', openEvent)
129 }
66a7748d 130 this.ws.onmessage = this.responseHandler.bind(this)
a974c8e4 131 this.ws.onerror = errorEvent => {
66a7748d
JB
132 console.error('WebSocket error: ', errorEvent)
133 }
a974c8e4 134 this.ws.onclose = closeEvent => {
66a7748d
JB
135 console.info('WebSocket closed: ', closeEvent)
136 }
5a010bf0
JB
137 }
138
757b2ecf 139 private async sendRequest(
8d6f4790
JB
140 procedureName: ProcedureName,
141 payload: RequestPayload
757b2ecf 142 ): Promise<ResponsePayload> {
5b2721db 143 return new Promise<ResponsePayload>((resolve, reject) => {
1b2acf4e 144 if (this.ws.readyState !== WebSocket.OPEN) {
66a7748d 145 this.openWS()
1b2acf4e
JB
146 }
147 if (this.ws.readyState === WebSocket.OPEN) {
66a7748d 148 const uuid = crypto.randomUUID()
8d6f4790 149 const msg = JSON.stringify([uuid, procedureName, payload])
1a32c36b 150 const sendTimeout = setTimeout(() => {
8d6f4790 151 this.responseHandlers.delete(uuid)
e2372e58 152 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
9d76f5ec 153 }, 60000)
1a32c36b 154 try {
66a7748d 155 this.ws.send(msg)
8d6f4790 156 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
1a32c36b 157 } catch (error) {
8d6f4790 158 this.responseHandlers.delete(uuid)
66a7748d 159 reject(error)
1a32c36b 160 } finally {
66a7748d 161 clearTimeout(sendTimeout)
1a32c36b 162 }
1b2acf4e 163 } else {
e2372e58 164 reject(new Error(`Send request '${procedureName}' message: connection closed`))
1b2acf4e 165 }
66a7748d 166 })
32de5a57
LM
167 }
168
97f0a1a5 169 private responseHandler(messageEvent: MessageEvent<string>): void {
66a7748d 170 const response = JSON.parse(messageEvent.data) as ProtocolResponse
32de5a57 171
5e3cb728 172 if (Array.isArray(response) === false) {
66a7748d 173 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
32de5a57
LM
174 }
175
66a7748d 176 const [uuid, responsePayload] = response
32de5a57 177
12f26d4a 178 if (this.responseHandlers.has(uuid) === true) {
8d6f4790 179 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
5e3cb728 180 switch (responsePayload.status) {
32de5a57 181 case ResponseStatus.SUCCESS:
66a7748d
JB
182 resolve(responsePayload)
183 break
32de5a57 184 case ResponseStatus.FAILURE:
66a7748d
JB
185 reject(responsePayload)
186 break
32de5a57 187 default:
82fa1110 188 console.error(
66a7748d
JB
189 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
190 )
32de5a57 191 }
8d6f4790 192 this.responseHandlers.delete(uuid)
32de5a57 193 } else {
66a7748d 194 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
32de5a57
LM
195 }
196 }
197}