fix: ensure add charging stations reponse display only the necessary
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
CommitLineData
66a7748d 1import { BaseError, type OCPPError } from '../../../exception/index.js'
675fa8e3 2import {
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 20import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
66a7748d
JB
21import { Bootstrap } from '../../Bootstrap.js'
22import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
23import type { AbstractUIServer } from '../AbstractUIServer.js'
4198ad5c 24
66a7748d 25const moduleName = 'AbstractUIService'
32de5a57 26
1091427e
JB
27interface AddChargingStationsRequestPayload extends RequestPayload {
28 template: string
29 numberOfStations: number
30 options?: ChargingStationOptions
31}
32
268a74bb 33export 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 })
a974c8e4 226 .filter(hashId => hashId != null) as string[]
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 {
1091427e 304 status: failedStationInfos.length > 0 ? 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}