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