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