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