Commit | Line | Data |
---|---|---|
0f71040c | 1 | import { |
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 | |
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 { |
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: |
7c1d037f JB |
215 | reject( |
216 | new Error( | |
217 | `Response status for procedure '${procedureName}' not supported: '${responsePayload.status}'` | |
218 | ) | |
66a7748d | 219 | ) |
32de5a57 | 220 | } |
8d6f4790 | 221 | this.responseHandlers.delete(uuid) |
32de5a57 | 222 | } else { |
66a7748d | 223 | throw new Error(`Not a response to a request: ${JSON.stringify(response, undefined, 2)}`) |
32de5a57 LM |
224 | } |
225 | } | |
226 | } |