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