Commit | Line | Data |
---|---|---|
66a7748d | 1 | import { BaseError, type OCPPError } from '../../../exception/index.js' |
675fa8e3 | 2 | import { |
268a74bb JB |
3 | BroadcastChannelProcedureName, |
4 | type BroadcastChannelRequestPayload, | |
3b09e788 | 5 | type ChargingStationInfo, |
71ac2bd7 | 6 | type ChargingStationOptions, |
a66bbcfe | 7 | ConfigurationSection, |
61877a2e | 8 | type JsonObject, |
66a7748d | 9 | type JsonType, |
32de5a57 | 10 | ProcedureName, |
976d11ec JB |
11 | type ProtocolRequest, |
12 | type ProtocolRequestHandler, | |
f130b8e6 | 13 | type ProtocolResponse, |
976d11ec JB |
14 | type ProtocolVersion, |
15 | type RequestPayload, | |
16 | type ResponsePayload, | |
a66bbcfe JB |
17 | ResponseStatus, |
18 | type StorageConfiguration | |
66a7748d | 19 | } from '../../../types/index.js' |
a66bbcfe | 20 | import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js' |
66a7748d JB |
21 | import { Bootstrap } from '../../Bootstrap.js' |
22 | import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js' | |
23 | import type { AbstractUIServer } from '../AbstractUIServer.js' | |
4198ad5c | 24 | |
66a7748d | 25 | const moduleName = 'AbstractUIService' |
32de5a57 | 26 | |
1091427e JB |
27 | interface AddChargingStationsRequestPayload extends RequestPayload { |
28 | template: string | |
29 | numberOfStations: number | |
30 | options?: ChargingStationOptions | |
31 | } | |
32 | ||
268a74bb | 33 | export abstract class AbstractUIService { |
a37fc6dc | 34 | protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map< |
66a7748d JB |
35 | ProcedureName, |
36 | BroadcastChannelProcedureName | |
a37fc6dc JB |
37 | >([ |
38 | [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION], | |
39 | [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION], | |
09e5a7a8 JB |
40 | [ |
41 | ProcedureName.DELETE_CHARGING_STATIONS, | |
42 | BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS | |
43 | ], | |
a37fc6dc JB |
44 | [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION], |
45 | [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION], | |
46 | [ | |
47 | ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR, | |
66a7748d | 48 | BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR |
a37fc6dc JB |
49 | ], |
50 | [ | |
51 | ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR, | |
66a7748d | 52 | BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR |
a37fc6dc JB |
53 | ], |
54 | [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL], | |
55 | [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION], | |
56 | [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION], | |
57 | [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE], | |
58 | [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION], | |
59 | [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION], | |
60 | [ProcedureName.HEARTBEAT, BroadcastChannelProcedureName.HEARTBEAT], | |
61 | [ProcedureName.METER_VALUES, BroadcastChannelProcedureName.METER_VALUES], | |
62 | [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER], | |
63 | [ | |
64 | ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION, | |
66a7748d | 65 | BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION |
a37fc6dc JB |
66 | ], |
67 | [ | |
68 | ProcedureName.FIRMWARE_STATUS_NOTIFICATION, | |
66a7748d JB |
69 | BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION |
70 | ] | |
71 | ]) | |
72 | ||
73 | protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler> | |
74 | private readonly version: ProtocolVersion | |
75 | private readonly uiServer: AbstractUIServer | |
76 | private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel | |
2c5c7443 JB |
77 | private readonly broadcastChannelRequests: Map< |
78 | `${string}-${string}-${string}-${string}-${string}`, | |
79 | number | |
80 | > | |
66a7748d JB |
81 | |
82 | constructor (uiServer: AbstractUIServer, version: ProtocolVersion) { | |
83 | this.uiServer = uiServer | |
84 | this.version = version | |
02a6943a | 85 | this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([ |
42e341c4 | 86 | [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)], |
32de5a57 | 87 | [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)], |
c5ecc04d | 88 | [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)], |
a66bbcfe | 89 | [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)], |
240fa4da | 90 | [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)], |
32de5a57 | 91 | [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)], |
66a7748d JB |
92 | [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)] |
93 | ]) | |
94 | this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this) | |
2c5c7443 JB |
95 | this.broadcastChannelRequests = new Map< |
96 | `${string}-${string}-${string}-${string}-${string}`, | |
97 | number | |
98 | >() | |
4198ad5c JB |
99 | } |
100 | ||
09e5a7a8 JB |
101 | public stop (): void { |
102 | this.broadcastChannelRequests.clear() | |
103 | this.uiServiceWorkerBroadcastChannel.close() | |
104 | } | |
105 | ||
66a7748d | 106 | public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> { |
2c5c7443 | 107 | let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined |
66a7748d JB |
108 | let command: ProcedureName | undefined |
109 | let requestPayload: RequestPayload | undefined | |
110 | let responsePayload: ResponsePayload | undefined | |
32de5a57 | 111 | try { |
2c5c7443 | 112 | [uuid, command, requestPayload] = request |
32de5a57 | 113 | |
66a7748d | 114 | if (!this.requestHandlers.has(command)) { |
32de5a57 | 115 | throw new BaseError( |
240fa4da | 116 | `'${command}' is not implemented to handle message payload ${JSON.stringify( |
32de5a57 | 117 | requestPayload, |
4ed03b6e | 118 | undefined, |
66a7748d JB |
119 | 2 |
120 | )}` | |
121 | ) | |
4198ad5c | 122 | } |
89b7a234 | 123 | |
6c8f5d90 | 124 | // Call the request handler to build the response payload |
66a7748d | 125 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
4b9332af JB |
126 | const requestHandler = this.requestHandlers.get(command)! |
127 | if (isAsyncFunction(requestHandler)) { | |
2c5c7443 | 128 | responsePayload = await requestHandler(uuid, command, requestPayload) |
4b9332af JB |
129 | } else { |
130 | responsePayload = ( | |
131 | requestHandler as ( | |
132 | uuid?: string, | |
133 | procedureName?: ProcedureName, | |
134 | payload?: RequestPayload | |
135 | ) => undefined | ResponsePayload | |
2c5c7443 | 136 | )(uuid, command, requestPayload) |
4b9332af | 137 | } |
32de5a57 LM |
138 | } catch (error) { |
139 | // Log | |
66a7748d | 140 | logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error) |
6c8f5d90 | 141 | responsePayload = { |
551e477c | 142 | hashIds: requestPayload?.hashIds, |
6c8f5d90 JB |
143 | status: ResponseStatus.FAILURE, |
144 | command, | |
145 | requestPayload, | |
146 | responsePayload, | |
7375968c JB |
147 | errorMessage: (error as OCPPError).message, |
148 | errorStack: (error as OCPPError).stack, | |
66a7748d | 149 | errorDetails: (error as OCPPError).details |
c5ecc04d | 150 | } satisfies ResponsePayload |
f130b8e6 | 151 | } |
aa63c9b7 | 152 | if (responsePayload != null) { |
66a7748d | 153 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
2c5c7443 | 154 | return this.uiServer.buildProtocolResponse(uuid!, responsePayload) |
4198ad5c | 155 | } |
6c8f5d90 JB |
156 | } |
157 | ||
66a7748d | 158 | // public sendRequest ( |
2c5c7443 | 159 | // uuid: `${string}-${string}-${string}-${string}-${string}`, |
0b22144c | 160 | // procedureName: ProcedureName, |
66a7748d | 161 | // requestPayload: RequestPayload |
0b22144c JB |
162 | // ): void { |
163 | // this.uiServer.sendRequest( | |
2c5c7443 | 164 | // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload) |
66a7748d | 165 | // ) |
0b22144c | 166 | // } |
32de5a57 | 167 | |
2c5c7443 JB |
168 | public sendResponse ( |
169 | uuid: `${string}-${string}-${string}-${string}-${string}`, | |
170 | responsePayload: ResponsePayload | |
171 | ): void { | |
172 | if (this.uiServer.hasResponseHandler(uuid)) { | |
173 | this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(uuid, responsePayload)) | |
1ca4a038 | 174 | } |
4198ad5c JB |
175 | } |
176 | ||
8b7072dc | 177 | public logPrefix = (modName: string, methodName: string): string => { |
66a7748d JB |
178 | return this.uiServer.logPrefix(modName, methodName, this.version) |
179 | } | |
0d2cec76 | 180 | |
2c5c7443 JB |
181 | public deleteBroadcastChannelRequest ( |
182 | uuid: `${string}-${string}-${string}-${string}-${string}` | |
183 | ): void { | |
66a7748d | 184 | this.broadcastChannelRequests.delete(uuid) |
0d2cec76 JB |
185 | } |
186 | ||
2c5c7443 JB |
187 | public getBroadcastChannelExpectedResponses ( |
188 | uuid: `${string}-${string}-${string}-${string}-${string}` | |
189 | ): number { | |
66a7748d JB |
190 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
191 | return this.broadcastChannelRequests.get(uuid)! | |
0d2cec76 JB |
192 | } |
193 | ||
66a7748d | 194 | protected handleProtocolRequest ( |
2c5c7443 | 195 | uuid: `${string}-${string}-${string}-${string}-${string}`, |
2a3cf7fc | 196 | procedureName: ProcedureName, |
66a7748d | 197 | payload: RequestPayload |
2a3cf7fc JB |
198 | ): void { |
199 | this.sendBroadcastChannelRequest( | |
200 | uuid, | |
66a7748d | 201 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
a37fc6dc | 202 | AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!, |
66a7748d JB |
203 | payload |
204 | ) | |
2a3cf7fc JB |
205 | } |
206 | ||
66a7748d | 207 | private sendBroadcastChannelRequest ( |
2c5c7443 | 208 | uuid: `${string}-${string}-${string}-${string}-${string}`, |
0d2cec76 | 209 | procedureName: BroadcastChannelProcedureName, |
66a7748d | 210 | payload: BroadcastChannelRequestPayload |
0d2cec76 | 211 | ): void { |
9bf0ef23 | 212 | if (isNotEmptyArray(payload.hashIds)) { |
0d2cec76 | 213 | payload.hashIds = payload.hashIds |
5dc7c990 | 214 | .map(hashId => { |
5199f9fd | 215 | if (this.uiServer.chargingStations.has(hashId)) { |
66a7748d | 216 | return hashId |
0d2cec76 | 217 | } |
3a6ef20a JB |
218 | logger.warn( |
219 | `${this.logPrefix( | |
220 | moduleName, | |
66a7748d JB |
221 | 'sendBroadcastChannelRequest' |
222 | )} Charging station with hashId '${hashId}' not found` | |
223 | ) | |
224 | return undefined | |
f12cf7ef | 225 | }) |
52f298cf | 226 | .filter(hashId => hashId != null) |
3a6ef20a | 227 | } else { |
66a7748d | 228 | delete payload.hashIds |
0d2cec76 | 229 | } |
3a6ef20a JB |
230 | const expectedNumberOfResponses = Array.isArray(payload.hashIds) |
231 | ? payload.hashIds.length | |
66a7748d | 232 | : this.uiServer.chargingStations.size |
3a6ef20a JB |
233 | if (expectedNumberOfResponses === 0) { |
234 | throw new BaseError( | |
66a7748d JB |
235 | 'hashIds array in the request payload does not contain any valid charging station hashId' |
236 | ) | |
3a6ef20a | 237 | } |
66a7748d JB |
238 | this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload]) |
239 | this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses) | |
6c8f5d90 JB |
240 | } |
241 | ||
42e341c4 JB |
242 | private handleListTemplates (): ResponsePayload { |
243 | return { | |
244 | status: ResponseStatus.SUCCESS, | |
245 | templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[] | |
246 | } satisfies ResponsePayload | |
247 | } | |
248 | ||
66a7748d | 249 | private handleListChargingStations (): ResponsePayload { |
32de5a57 LM |
250 | return { |
251 | status: ResponseStatus.SUCCESS, | |
66a7748d JB |
252 | chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[] |
253 | } satisfies ResponsePayload | |
32de5a57 LM |
254 | } |
255 | ||
c5ecc04d | 256 | private async handleAddChargingStations ( |
2c5c7443 | 257 | _uuid?: `${string}-${string}-${string}-${string}-${string}`, |
0447d90f | 258 | _procedureName?: ProcedureName, |
c5ecc04d JB |
259 | requestPayload?: RequestPayload |
260 | ): Promise<ResponsePayload> { | |
1091427e JB |
261 | const { template, numberOfStations, options } = |
262 | requestPayload as AddChargingStationsRequestPayload | |
3b68e416 JB |
263 | if (!Bootstrap.getInstance().getState().started) { |
264 | return { | |
265 | status: ResponseStatus.FAILURE, | |
266 | errorMessage: | |
267 | 'Cannot add charging station(s) while the charging stations simulator is not started' | |
268 | } satisfies ResponsePayload | |
269 | } | |
270 | if (typeof template !== 'string' || typeof numberOfStations !== 'number') { | |
271 | return { | |
272 | status: ResponseStatus.FAILURE, | |
273 | errorMessage: 'Invalid request payload' | |
274 | } satisfies ResponsePayload | |
275 | } | |
c5ecc04d JB |
276 | if (!this.uiServer.chargingStationTemplates.has(template)) { |
277 | return { | |
278 | status: ResponseStatus.FAILURE, | |
279 | errorMessage: `Template '${template}' not found` | |
280 | } satisfies ResponsePayload | |
281 | } | |
1091427e JB |
282 | const succeededStationInfos: ChargingStationInfo[] = [] |
283 | const failedStationInfos: ChargingStationInfo[] = [] | |
284 | let err: Error | undefined | |
c5ecc04d | 285 | for (let i = 0; i < numberOfStations; i++) { |
3b09e788 | 286 | let stationInfo: ChargingStationInfo | undefined |
c5ecc04d | 287 | try { |
3b09e788 | 288 | stationInfo = await Bootstrap.getInstance().addChargingStation( |
c5ecc04d | 289 | Bootstrap.getInstance().getLastIndex(template) + 1, |
71ac2bd7 JB |
290 | `${template}.json`, |
291 | options | |
c5ecc04d | 292 | ) |
3b09e788 | 293 | if (stationInfo != null) { |
1091427e | 294 | succeededStationInfos.push(stationInfo) |
3b09e788 | 295 | } |
c5ecc04d | 296 | } catch (error) { |
1091427e JB |
297 | err = error as Error |
298 | if (stationInfo != null) { | |
299 | failedStationInfos.push(stationInfo) | |
300 | } | |
c5ecc04d JB |
301 | } |
302 | } | |
303 | return { | |
43c49f23 | 304 | status: err != null ? ResponseStatus.FAILURE : ResponseStatus.SUCCESS, |
2ea8735d JB |
305 | ...(succeededStationInfos.length > 0 && { |
306 | hashIdsSucceeded: succeededStationInfos.map(stationInfo => stationInfo.hashId) | |
307 | }), | |
1091427e JB |
308 | ...(failedStationInfos.length > 0 && { |
309 | hashIdsFailed: failedStationInfos.map(stationInfo => stationInfo.hashId) | |
310 | }), | |
311 | ...(err != null && { errorMessage: err.message, errorStack: err.stack }) | |
312 | } satisfies ResponsePayload | |
c5ecc04d JB |
313 | } |
314 | ||
a66bbcfe JB |
315 | private handlePerformanceStatistics (): ResponsePayload { |
316 | if ( | |
317 | Configuration.getConfigurationSection<StorageConfiguration>( | |
318 | ConfigurationSection.performanceStorage | |
319 | ).enabled !== true | |
320 | ) { | |
321 | return { | |
322 | status: ResponseStatus.FAILURE, | |
323 | errorMessage: 'Performance statistics storage is not enabled' | |
324 | } satisfies ResponsePayload | |
325 | } | |
326 | try { | |
327 | return { | |
328 | status: ResponseStatus.SUCCESS, | |
329 | performanceStatistics: [ | |
330 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | |
331 | ...Bootstrap.getInstance().getPerformanceStatistics()! | |
332 | ] as JsonType[] | |
1091427e | 333 | } satisfies ResponsePayload |
a66bbcfe JB |
334 | } catch (error) { |
335 | return { | |
336 | status: ResponseStatus.FAILURE, | |
337 | errorMessage: (error as Error).message, | |
338 | errorStack: (error as Error).stack | |
339 | } satisfies ResponsePayload | |
340 | } | |
341 | } | |
342 | ||
240fa4da JB |
343 | private handleSimulatorState (): ResponsePayload { |
344 | try { | |
345 | return { | |
346 | status: ResponseStatus.SUCCESS, | |
61877a2e | 347 | state: Bootstrap.getInstance().getState() as unknown as JsonObject |
240fa4da JB |
348 | } satisfies ResponsePayload |
349 | } catch (error) { | |
350 | return { | |
351 | status: ResponseStatus.FAILURE, | |
352 | errorMessage: (error as Error).message, | |
353 | errorStack: (error as Error).stack | |
354 | } satisfies ResponsePayload | |
355 | } | |
356 | } | |
357 | ||
66a7748d | 358 | private async handleStartSimulator (): Promise<ResponsePayload> { |
4ec634b7 | 359 | try { |
66a7748d JB |
360 | await Bootstrap.getInstance().start() |
361 | return { status: ResponseStatus.SUCCESS } | |
a66bbcfe JB |
362 | } catch (error) { |
363 | return { | |
364 | status: ResponseStatus.FAILURE, | |
365 | errorMessage: (error as Error).message, | |
366 | errorStack: (error as Error).stack | |
367 | } satisfies ResponsePayload | |
4ec634b7 | 368 | } |
32de5a57 LM |
369 | } |
370 | ||
66a7748d | 371 | private async handleStopSimulator (): Promise<ResponsePayload> { |
4ec634b7 | 372 | try { |
66a7748d JB |
373 | await Bootstrap.getInstance().stop() |
374 | return { status: ResponseStatus.SUCCESS } | |
a66bbcfe JB |
375 | } catch (error) { |
376 | return { | |
377 | status: ResponseStatus.FAILURE, | |
378 | errorMessage: (error as Error).message, | |
379 | errorStack: (error as Error).stack | |
380 | } satisfies ResponsePayload | |
4ec634b7 | 381 | } |
4198ad5c JB |
382 | } |
383 | } |