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