6652def2ad2f64e5a43cf30fb1ab0831a9dfdcda
[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 export abstract class AbstractUIService {
28 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
29 ProcedureName,
30 BroadcastChannelProcedureName
31 >([
32 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
33 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
34 [
35 ProcedureName.DELETE_CHARGING_STATIONS,
36 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS
37 ],
38 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
39 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
40 [
41 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
42 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
43 ],
44 [
45 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
46 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
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,
59 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
60 ],
61 [
62 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
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
71 private readonly broadcastChannelRequests: Map<
72 `${string}-${string}-${string}-${string}-${string}`,
73 number
74 >
75
76 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
77 this.uiServer = uiServer
78 this.version = version
79 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
80 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
81 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
82 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
83 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
84 [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
85 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
86 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
87 ])
88 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
89 this.broadcastChannelRequests = new Map<
90 `${string}-${string}-${string}-${string}-${string}`,
91 number
92 >()
93 }
94
95 public stop (): void {
96 this.broadcastChannelRequests.clear()
97 this.uiServiceWorkerBroadcastChannel.close()
98 }
99
100 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
101 let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined
102 let command: ProcedureName | undefined
103 let requestPayload: RequestPayload | undefined
104 let responsePayload: ResponsePayload | undefined
105 try {
106 [uuid, command, requestPayload] = request
107
108 if (!this.requestHandlers.has(command)) {
109 throw new BaseError(
110 `'${command}' is not implemented to handle message payload ${JSON.stringify(
111 requestPayload,
112 undefined,
113 2
114 )}`
115 )
116 }
117
118 // Call the request handler to build the response payload
119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120 const requestHandler = this.requestHandlers.get(command)!
121 if (isAsyncFunction(requestHandler)) {
122 responsePayload = await requestHandler(uuid, command, requestPayload)
123 } else {
124 responsePayload = (
125 requestHandler as (
126 uuid?: string,
127 procedureName?: ProcedureName,
128 payload?: RequestPayload
129 ) => undefined | ResponsePayload
130 )(uuid, command, requestPayload)
131 }
132 } catch (error) {
133 // Log
134 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
135 responsePayload = {
136 hashIds: requestPayload?.hashIds,
137 status: ResponseStatus.FAILURE,
138 command,
139 requestPayload,
140 responsePayload,
141 errorMessage: (error as OCPPError).message,
142 errorStack: (error as OCPPError).stack,
143 errorDetails: (error as OCPPError).details
144 } satisfies ResponsePayload
145 }
146 if (responsePayload != null) {
147 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148 return this.uiServer.buildProtocolResponse(uuid!, responsePayload)
149 }
150 }
151
152 // public sendRequest (
153 // uuid: `${string}-${string}-${string}-${string}-${string}`,
154 // procedureName: ProcedureName,
155 // requestPayload: RequestPayload
156 // ): void {
157 // this.uiServer.sendRequest(
158 // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
159 // )
160 // }
161
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))
168 }
169 }
170
171 public logPrefix = (modName: string, methodName: string): string => {
172 return this.uiServer.logPrefix(modName, methodName, this.version)
173 }
174
175 public deleteBroadcastChannelRequest (
176 uuid: `${string}-${string}-${string}-${string}-${string}`
177 ): void {
178 this.broadcastChannelRequests.delete(uuid)
179 }
180
181 public getBroadcastChannelExpectedResponses (
182 uuid: `${string}-${string}-${string}-${string}-${string}`
183 ): number {
184 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
185 return this.broadcastChannelRequests.get(uuid)!
186 }
187
188 protected handleProtocolRequest (
189 uuid: `${string}-${string}-${string}-${string}-${string}`,
190 procedureName: ProcedureName,
191 payload: RequestPayload
192 ): void {
193 this.sendBroadcastChannelRequest(
194 uuid,
195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
196 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
197 payload
198 )
199 }
200
201 private sendBroadcastChannelRequest (
202 uuid: `${string}-${string}-${string}-${string}-${string}`,
203 procedureName: BroadcastChannelProcedureName,
204 payload: BroadcastChannelRequestPayload
205 ): void {
206 if (isNotEmptyArray(payload.hashIds)) {
207 payload.hashIds = payload.hashIds
208 .map(hashId => {
209 if (this.uiServer.chargingStations.has(hashId)) {
210 return hashId
211 }
212 logger.warn(
213 `${this.logPrefix(
214 moduleName,
215 'sendBroadcastChannelRequest'
216 )} Charging station with hashId '${hashId}' not found`
217 )
218 return undefined
219 })
220 .filter(hashId => hashId != null) as string[]
221 } else {
222 delete payload.hashIds
223 }
224 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
225 ? payload.hashIds.length
226 : this.uiServer.chargingStations.size
227 if (expectedNumberOfResponses === 0) {
228 throw new BaseError(
229 'hashIds array in the request payload does not contain any valid charging station hashId'
230 )
231 }
232 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
233 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
234 }
235
236 private handleListTemplates (): ResponsePayload {
237 return {
238 status: ResponseStatus.SUCCESS,
239 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
240 } satisfies ResponsePayload
241 }
242
243 private handleListChargingStations (): ResponsePayload {
244 return {
245 status: ResponseStatus.SUCCESS,
246 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
247 } satisfies ResponsePayload
248 }
249
250 private async handleAddChargingStations (
251 _uuid?: `${string}-${string}-${string}-${string}-${string}`,
252 _procedureName?: ProcedureName,
253 requestPayload?: RequestPayload
254 ): Promise<ResponsePayload> {
255 const { template, numberOfStations, options } = requestPayload as {
256 template: string
257 numberOfStations: number
258 options?: ChargingStationOptions
259 }
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 }
273 if (!this.uiServer.chargingStationTemplates.has(template)) {
274 return {
275 status: ResponseStatus.FAILURE,
276 errorMessage: `Template '${template}' not found`
277 } satisfies ResponsePayload
278 }
279 const stationInfos: ChargingStationInfo[] = []
280 for (let i = 0; i < numberOfStations; i++) {
281 let stationInfo: ChargingStationInfo | undefined
282 try {
283 stationInfo = await Bootstrap.getInstance().addChargingStation(
284 Bootstrap.getInstance().getLastIndex(template) + 1,
285 `${template}.json`,
286 options
287 )
288 if (stationInfo != null) {
289 stationInfos.push(stationInfo)
290 }
291 } catch (error) {
292 return {
293 status: ResponseStatus.FAILURE,
294 ...(stationInfo?.hashId != null && { hashIdsFailed: [stationInfo.hashId] }),
295 errorMessage: (error as Error).message,
296 errorStack: (error as Error).stack
297 } satisfies ResponsePayload
298 }
299 }
300 return {
301 status: ResponseStatus.SUCCESS,
302 hashIdsSucceeded: stationInfos.map(stationInfo => stationInfo.hashId)
303 }
304 }
305
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
334 private handleSimulatorState (): ResponsePayload {
335 try {
336 return {
337 status: ResponseStatus.SUCCESS,
338 state: Bootstrap.getInstance().getState() as unknown as JsonObject
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
349 private async handleStartSimulator (): Promise<ResponsePayload> {
350 try {
351 await Bootstrap.getInstance().start()
352 return { status: ResponseStatus.SUCCESS }
353 } catch (error) {
354 return {
355 status: ResponseStatus.FAILURE,
356 errorMessage: (error as Error).message,
357 errorStack: (error as Error).stack
358 } satisfies ResponsePayload
359 }
360 }
361
362 private async handleStopSimulator (): Promise<ResponsePayload> {
363 try {
364 await Bootstrap.getInstance().stop()
365 return { status: ResponseStatus.SUCCESS }
366 } catch (error) {
367 return {
368 status: ResponseStatus.FAILURE,
369 errorMessage: (error as Error).message,
370 errorStack: (error as Error).stack
371 } satisfies ResponsePayload
372 }
373 }
374 }