Commit | Line | Data |
---|---|---|
0f71040c | 1 | import { |
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 | |
12 | type ResponseHandler = { | |
66a7748d JB |
13 | procedureName: ProcedureName |
14 | resolve: (value: ResponsePayload | PromiseLike<ResponsePayload>) => void | |
15 | reject: (reason?: unknown) => void | |
16 | } | |
32de5a57 | 17 | |
8137295e | 18 | export 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 | } |