refactor(ui): refine application title
[e-mobility-charging-stations-simulator.git] / ui / web / src / composables / UIClient.ts
CommitLineData
0f71040c 1import {
217db058 2 ApplicationProtocol,
329eab0e 3 AuthenticationType,
0f71040c 4 ProcedureName,
82a77234 5 type ProtocolResponse,
0f71040c
JB
6 type RequestPayload,
7 type ResponsePayload,
f292861c
JB
8 ResponseStatus,
9 type UIServerConfigurationSection
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 {
f292861c 19 private static readonly instances: Map<number, UIClient> = new Map<number, UIClient>()
32de5a57 20
66a7748d
JB
21 private ws!: WebSocket
22 private responseHandlers: Map<string, ResponseHandler>
32de5a57 23
f292861c 24 private constructor(private uiServerConfiguration: UIServerConfigurationSection) {
66a7748d
JB
25 this.openWS()
26 this.responseHandlers = new Map<string, ResponseHandler>()
32de5a57
LM
27 }
28
f292861c
JB
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}`)
32de5a57 37 }
f292861c 38 return UIClient.instances.get(serverId)!
32de5a57
LM
39 }
40
ca1e5439
JB
41 public registerWSEventListener<K extends keyof WebSocketEventMap>(
42 event: K,
43 listener: (event: WebSocketEventMap[K]) => void
44 ) {
45 this.ws.addEventListener(event, listener)
32de5a57
LM
46 }
47
5a010bf0 48 public async startSimulator(): Promise<ResponsePayload> {
66a7748d 49 return this.sendRequest(ProcedureName.START_SIMULATOR, {})
5a010bf0
JB
50 }
51
52 public async stopSimulator(): Promise<ResponsePayload> {
66a7748d 53 return this.sendRequest(ProcedureName.STOP_SIMULATOR, {})
5a010bf0 54 }
32de5a57 55
c317ae3e
JB
56 public async listTemplates(): Promise<ResponsePayload> {
57 return this.sendRequest(ProcedureName.LIST_TEMPLATES, {})
a64b9a64
JB
58 }
59
5a010bf0 60 public async listChargingStations(): Promise<ResponsePayload> {
66a7748d 61 return this.sendRequest(ProcedureName.LIST_CHARGING_STATIONS, {})
32de5a57
LM
62 }
63
c317ae3e
JB
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
f8696170
JB
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
8fc2e5cc 82 public async startChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 83 return this.sendRequest(ProcedureName.START_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
84 }
85
86 public async stopChargingStation(hashId: string): Promise<ResponsePayload> {
66a7748d 87 return this.sendRequest(ProcedureName.STOP_CHARGING_STATION, { hashIds: [hashId] })
8fc2e5cc
JB
88 }
89
90 public async openConnection(hashId: string): Promise<ResponsePayload> {
91 return this.sendRequest(ProcedureName.OPEN_CONNECTION, {
a974c8e4 92 hashIds: [hashId]
66a7748d 93 })
8fc2e5cc
JB
94 }
95
96 public async closeConnection(hashId: string): Promise<ResponsePayload> {
97 return this.sendRequest(ProcedureName.CLOSE_CONNECTION, {
a974c8e4 98 hashIds: [hashId]
66a7748d 99 })
8fc2e5cc
JB
100 }
101
32de5a57
LM
102 public async startTransaction(
103 hashId: string,
104 connectorId: number,
66a7748d 105 idTag: string | undefined
32de5a57 106 ): Promise<ResponsePayload> {
32de5a57 107 return this.sendRequest(ProcedureName.START_TRANSACTION, {
757b2ecf 108 hashIds: [hashId],
32de5a57 109 connectorId,
a974c8e4 110 idTag
66a7748d 111 })
32de5a57
LM
112 }
113
5a010bf0
JB
114 public async stopTransaction(
115 hashId: string,
66a7748d 116 transactionId: number | undefined
5a010bf0 117 ): Promise<ResponsePayload> {
32de5a57 118 return this.sendRequest(ProcedureName.STOP_TRANSACTION, {
757b2ecf 119 hashIds: [hashId],
a974c8e4 120 transactionId
66a7748d 121 })
32de5a57
LM
122 }
123
757b2ecf
JB
124 public async startAutomaticTransactionGenerator(
125 hashId: string,
66a7748d 126 connectorId: number
757b2ecf
JB
127 ): Promise<ResponsePayload> {
128 return this.sendRequest(ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, {
129 hashIds: [hashId],
a974c8e4 130 connectorIds: [connectorId]
66a7748d 131 })
757b2ecf
JB
132 }
133
134 public async stopAutomaticTransactionGenerator(
135 hashId: string,
66a7748d 136 connectorId: number
757b2ecf
JB
137 ): Promise<ResponsePayload> {
138 return this.sendRequest(ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, {
139 hashIds: [hashId],
a974c8e4 140 connectorIds: [connectorId]
66a7748d 141 })
757b2ecf
JB
142 }
143
9e1d6e03 144 private openWS(): void {
329eab0e 145 const protocols =
f292861c
JB
146 this.uiServerConfiguration.authentication?.enabled === true &&
147 this.uiServerConfiguration.authentication?.type === AuthenticationType.PROTOCOL_BASIC_AUTH
329eab0e 148 ? [
f292861c
JB
149 `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`,
150 `authorization.basic.${btoa(`${this.uiServerConfiguration.authentication.username}:${this.uiServerConfiguration.authentication.password}`).replace(/={1,2}$/, '')}`
329eab0e 151 ]
f292861c 152 : `${this.uiServerConfiguration.protocol}${this.uiServerConfiguration.version}`
12f26d4a 153 this.ws = new WebSocket(
f292861c 154 `${this.uiServerConfiguration.secure === true ? ApplicationProtocol.WSS : ApplicationProtocol.WS}://${this.uiServerConfiguration.host}:${this.uiServerConfiguration.port}`,
329eab0e 155 protocols
66a7748d 156 )
e2372e58
JB
157 this.ws.onopen = openEvent => {
158 console.info('WebSocket opened', openEvent)
159 }
66a7748d 160 this.ws.onmessage = this.responseHandler.bind(this)
a974c8e4 161 this.ws.onerror = errorEvent => {
66a7748d
JB
162 console.error('WebSocket error: ', errorEvent)
163 }
a974c8e4 164 this.ws.onclose = closeEvent => {
66a7748d
JB
165 console.info('WebSocket closed: ', closeEvent)
166 }
5a010bf0
JB
167 }
168
757b2ecf 169 private async sendRequest(
8d6f4790
JB
170 procedureName: ProcedureName,
171 payload: RequestPayload
757b2ecf 172 ): Promise<ResponsePayload> {
5b2721db 173 return new Promise<ResponsePayload>((resolve, reject) => {
1b2acf4e 174 if (this.ws.readyState === WebSocket.OPEN) {
66a7748d 175 const uuid = crypto.randomUUID()
8d6f4790 176 const msg = JSON.stringify([uuid, procedureName, payload])
1a32c36b 177 const sendTimeout = setTimeout(() => {
8d6f4790 178 this.responseHandlers.delete(uuid)
e2372e58 179 return reject(new Error(`Send request '${procedureName}' message: connection timeout`))
9d76f5ec 180 }, 60000)
1a32c36b 181 try {
66a7748d 182 this.ws.send(msg)
8d6f4790 183 this.responseHandlers.set(uuid, { procedureName, resolve, reject })
1a32c36b 184 } catch (error) {
8d6f4790 185 this.responseHandlers.delete(uuid)
66a7748d 186 reject(error)
1a32c36b 187 } finally {
66a7748d 188 clearTimeout(sendTimeout)
1a32c36b 189 }
1b2acf4e 190 } else {
e2372e58 191 reject(new Error(`Send request '${procedureName}' message: connection closed`))
1b2acf4e 192 }
66a7748d 193 })
32de5a57
LM
194 }
195
97f0a1a5 196 private responseHandler(messageEvent: MessageEvent<string>): void {
66a7748d 197 const response = JSON.parse(messageEvent.data) as ProtocolResponse
32de5a57 198
5e3cb728 199 if (Array.isArray(response) === false) {
66a7748d 200 throw new Error(`Response not an array: ${JSON.stringify(response, undefined, 2)}`)
32de5a57
LM
201 }
202
66a7748d 203 const [uuid, responsePayload] = response
32de5a57 204
12f26d4a 205 if (this.responseHandlers.has(uuid) === true) {
8d6f4790 206 const { procedureName, resolve, reject } = this.responseHandlers.get(uuid)!
5e3cb728 207 switch (responsePayload.status) {
32de5a57 208 case ResponseStatus.SUCCESS:
66a7748d
JB
209 resolve(responsePayload)
210 break
32de5a57 211 case ResponseStatus.FAILURE:
66a7748d
JB
212 reject(responsePayload)
213 break
32de5a57 214 default:
82fa1110 215 console.error(
66a7748d
JB
216 `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'`
217 )
32de5a57 218 }
8d6f4790 219 this.responseHandlers.delete(uuid)
32de5a57 220 } else {
66a7748d 221 throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`)
32de5a57
LM
222 }
223 }
224}