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