6ba08807b3afcecc895428cab5911797f9cbf718
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
1 import { BaseError, type OCPPError } from '../../../exception/index.js'
2 import {
3 BroadcastChannelProcedureName,
4 type BroadcastChannelRequestPayload,
5 type ChargingStationInfo,
6 type ChargingStationOptions,
7 ConfigurationSection,
8 type JsonObject,
9 type JsonType,
10 ProcedureName,
11 type ProtocolRequest,
12 type ProtocolRequestHandler,
13 type ProtocolResponse,
14 type ProtocolVersion,
15 type RequestPayload,
16 type ResponsePayload,
17 ResponseStatus,
18 type StorageConfiguration
19 } from '../../../types/index.js'
20 import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
21 import { Bootstrap } from '../../Bootstrap.js'
22 import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
23 import type { AbstractUIServer } from '../AbstractUIServer.js'
24
25 const moduleName = 'AbstractUIService'
26
27 interface AddChargingStationsRequestPayload extends RequestPayload {
28 template: string
29 numberOfStations: number
30 options?: ChargingStationOptions
31 }
32
33 export abstract class AbstractUIService {
34 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
35 ProcedureName,
36 BroadcastChannelProcedureName
37 >([
38 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
39 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
40 [
41 ProcedureName.DELETE_CHARGING_STATIONS,
42 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS
43 ],
44 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
45 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
46 [
47 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
48 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
49 ],
50 [
51 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
52 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
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,
65 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
66 ],
67 [
68 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
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
77 private readonly broadcastChannelRequests: Map<
78 `${string}-${string}-${string}-${string}-${string}`,
79 number
80 >
81
82 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
83 this.uiServer = uiServer
84 this.version = version
85 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
86 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
87 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
88 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
89 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
90 [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
91 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
92 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
93 ])
94 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
95 this.broadcastChannelRequests = new Map<
96 `${string}-${string}-${string}-${string}-${string}`,
97 number
98 >()
99 }
100
101 public stop (): void {
102 this.broadcastChannelRequests.clear()
103 this.uiServiceWorkerBroadcastChannel.close()
104 }
105
106 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
107 let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined
108 let command: ProcedureName | undefined
109 let requestPayload: RequestPayload | undefined
110 let responsePayload: ResponsePayload | undefined
111 try {
112 [uuid, command, requestPayload] = request
113
114 if (!this.requestHandlers.has(command)) {
115 throw new BaseError(
116 `'${command}' is not implemented to handle message payload ${JSON.stringify(
117 requestPayload,
118 undefined,
119 2
120 )}`
121 )
122 }
123
124 // Call the request handler to build the response payload
125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
126 const requestHandler = this.requestHandlers.get(command)!
127 if (isAsyncFunction(requestHandler)) {
128 responsePayload = await requestHandler(uuid, command, requestPayload)
129 } else {
130 responsePayload = (
131 requestHandler as (
132 uuid?: string,
133 procedureName?: ProcedureName,
134 payload?: RequestPayload
135 ) => undefined | ResponsePayload
136 )(uuid, command, requestPayload)
137 }
138 } catch (error) {
139 // Log
140 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
141 responsePayload = {
142 hashIds: requestPayload?.hashIds,
143 status: ResponseStatus.FAILURE,
144 command,
145 requestPayload,
146 responsePayload,
147 errorMessage: (error as OCPPError).message,
148 errorStack: (error as OCPPError).stack,
149 errorDetails: (error as OCPPError).details
150 } satisfies ResponsePayload
151 }
152 if (responsePayload != null) {
153 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154 return this.uiServer.buildProtocolResponse(uuid!, responsePayload)
155 }
156 }
157
158 // public sendRequest (
159 // uuid: `${string}-${string}-${string}-${string}-${string}`,
160 // procedureName: ProcedureName,
161 // requestPayload: RequestPayload
162 // ): void {
163 // this.uiServer.sendRequest(
164 // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
165 // )
166 // }
167
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))
174 }
175 }
176
177 public logPrefix = (modName: string, methodName: string): string => {
178 return this.uiServer.logPrefix(modName, methodName, this.version)
179 }
180
181 public deleteBroadcastChannelRequest (
182 uuid: `${string}-${string}-${string}-${string}-${string}`
183 ): void {
184 this.broadcastChannelRequests.delete(uuid)
185 }
186
187 public getBroadcastChannelExpectedResponses (
188 uuid: `${string}-${string}-${string}-${string}-${string}`
189 ): number {
190 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191 return this.broadcastChannelRequests.get(uuid)!
192 }
193
194 protected handleProtocolRequest (
195 uuid: `${string}-${string}-${string}-${string}-${string}`,
196 procedureName: ProcedureName,
197 payload: RequestPayload
198 ): void {
199 this.sendBroadcastChannelRequest(
200 uuid,
201 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
202 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
203 payload
204 )
205 }
206
207 private sendBroadcastChannelRequest (
208 uuid: `${string}-${string}-${string}-${string}-${string}`,
209 procedureName: BroadcastChannelProcedureName,
210 payload: BroadcastChannelRequestPayload
211 ): void {
212 if (isNotEmptyArray(payload.hashIds)) {
213 payload.hashIds = payload.hashIds
214 .map(hashId => {
215 if (this.uiServer.chargingStations.has(hashId)) {
216 return hashId
217 }
218 logger.warn(
219 `${this.logPrefix(
220 moduleName,
221 'sendBroadcastChannelRequest'
222 )} Charging station with hashId '${hashId}' not found`
223 )
224 return undefined
225 })
226 .filter(hashId => hashId != null) as string[]
227 } else {
228 delete payload.hashIds
229 }
230 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
231 ? payload.hashIds.length
232 : this.uiServer.chargingStations.size
233 if (expectedNumberOfResponses === 0) {
234 throw new BaseError(
235 'hashIds array in the request payload does not contain any valid charging station hashId'
236 )
237 }
238 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
239 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
240 }
241
242 private handleListTemplates (): ResponsePayload {
243 return {
244 status: ResponseStatus.SUCCESS,
245 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
246 } satisfies ResponsePayload
247 }
248
249 private handleListChargingStations (): ResponsePayload {
250 return {
251 status: ResponseStatus.SUCCESS,
252 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
253 } satisfies ResponsePayload
254 }
255
256 private async handleAddChargingStations (
257 _uuid?: `${string}-${string}-${string}-${string}-${string}`,
258 _procedureName?: ProcedureName,
259 requestPayload?: RequestPayload
260 ): Promise<ResponsePayload> {
261 const { template, numberOfStations, options } =
262 requestPayload as AddChargingStationsRequestPayload
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 }
276 if (!this.uiServer.chargingStationTemplates.has(template)) {
277 return {
278 status: ResponseStatus.FAILURE,
279 errorMessage: `Template '${template}' not found`
280 } satisfies ResponsePayload
281 }
282 const succeededStationInfos: ChargingStationInfo[] = []
283 const failedStationInfos: ChargingStationInfo[] = []
284 let err: Error | undefined
285 for (let i = 0; i < numberOfStations; i++) {
286 let stationInfo: ChargingStationInfo | undefined
287 try {
288 stationInfo = await Bootstrap.getInstance().addChargingStation(
289 Bootstrap.getInstance().getLastIndex(template) + 1,
290 `${template}.json`,
291 options
292 )
293 if (stationInfo != null) {
294 succeededStationInfos.push(stationInfo)
295 }
296 } catch (error) {
297 err = error as Error
298 if (stationInfo != null) {
299 failedStationInfos.push(stationInfo)
300 }
301 }
302 }
303 return {
304 status: failedStationInfos.length > 0 ? ResponseStatus.FAILURE : ResponseStatus.SUCCESS,
305 ...(succeededStationInfos.length > 0 && {
306 hashIdsSucceeded: succeededStationInfos.map(stationInfo => stationInfo.hashId)
307 }),
308 ...(failedStationInfos.length > 0 && {
309 hashIdsFailed: failedStationInfos.map(stationInfo => stationInfo.hashId)
310 }),
311 ...(err != null && { errorMessage: err.message, errorStack: err.stack })
312 } satisfies ResponsePayload
313 }
314
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[]
333 } satisfies ResponsePayload
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
343 private handleSimulatorState (): ResponsePayload {
344 try {
345 return {
346 status: ResponseStatus.SUCCESS,
347 state: Bootstrap.getInstance().getState() as unknown as JsonObject
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
358 private async handleStartSimulator (): Promise<ResponsePayload> {
359 try {
360 await Bootstrap.getInstance().start()
361 return { status: ResponseStatus.SUCCESS }
362 } catch (error) {
363 return {
364 status: ResponseStatus.FAILURE,
365 errorMessage: (error as Error).message,
366 errorStack: (error as Error).stack
367 } satisfies ResponsePayload
368 }
369 }
370
371 private async handleStopSimulator (): Promise<ResponsePayload> {
372 try {
373 await Bootstrap.getInstance().stop()
374 return { status: ResponseStatus.SUCCESS }
375 } catch (error) {
376 return {
377 status: ResponseStatus.FAILURE,
378 errorMessage: (error as Error).message,
379 errorStack: (error as Error).stack
380 } satisfies ResponsePayload
381 }
382 }
383 }