6aed70a1b555cfcf5718cc153d02edc3e25da894
[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 ChargingStationOptions,
6 ConfigurationSection,
7 type JsonObject,
8 type JsonType,
9 ProcedureName,
10 type ProtocolRequest,
11 type ProtocolRequestHandler,
12 type ProtocolResponse,
13 type ProtocolVersion,
14 type RequestPayload,
15 type ResponsePayload,
16 ResponseStatus,
17 type StorageConfiguration
18 } from '../../../types/index.js'
19 import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
20 import { Bootstrap } from '../../Bootstrap.js'
21 import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
22 import type { AbstractUIServer } from '../AbstractUIServer.js'
23
24 const moduleName = 'AbstractUIService'
25
26 export abstract class AbstractUIService {
27 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
28 ProcedureName,
29 BroadcastChannelProcedureName
30 >([
31 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
32 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
33 [
34 ProcedureName.DELETE_CHARGING_STATIONS,
35 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS
36 ],
37 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
38 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
39 [
40 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
41 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
42 ],
43 [
44 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
45 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
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,
58 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
59 ],
60 [
61 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
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
70 private readonly broadcastChannelRequests: Map<
71 `${string}-${string}-${string}-${string}-${string}`,
72 number
73 >
74
75 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
76 this.uiServer = uiServer
77 this.version = version
78 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
79 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
80 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
81 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
82 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
83 [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
84 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
85 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
86 ])
87 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
88 this.broadcastChannelRequests = new Map<
89 `${string}-${string}-${string}-${string}-${string}`,
90 number
91 >()
92 }
93
94 public stop (): void {
95 this.broadcastChannelRequests.clear()
96 this.uiServiceWorkerBroadcastChannel.close()
97 }
98
99 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
100 let uuid: `${string}-${string}-${string}-${string}-${string}` | undefined
101 let command: ProcedureName | undefined
102 let requestPayload: RequestPayload | undefined
103 let responsePayload: ResponsePayload | undefined
104 try {
105 [uuid, command, requestPayload] = request
106
107 if (!this.requestHandlers.has(command)) {
108 throw new BaseError(
109 `'${command}' is not implemented to handle message payload ${JSON.stringify(
110 requestPayload,
111 undefined,
112 2
113 )}`
114 )
115 }
116
117 // Call the request handler to build the response payload
118 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
119 const requestHandler = this.requestHandlers.get(command)!
120 if (isAsyncFunction(requestHandler)) {
121 responsePayload = await requestHandler(uuid, command, requestPayload)
122 } else {
123 responsePayload = (
124 requestHandler as (
125 uuid?: string,
126 procedureName?: ProcedureName,
127 payload?: RequestPayload
128 ) => undefined | ResponsePayload
129 )(uuid, command, requestPayload)
130 }
131 } catch (error) {
132 // Log
133 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
134 responsePayload = {
135 hashIds: requestPayload?.hashIds,
136 status: ResponseStatus.FAILURE,
137 command,
138 requestPayload,
139 responsePayload,
140 errorMessage: (error as OCPPError).message,
141 errorStack: (error as OCPPError).stack,
142 errorDetails: (error as OCPPError).details
143 } satisfies ResponsePayload
144 }
145 if (responsePayload != null) {
146 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147 return this.uiServer.buildProtocolResponse(uuid!, responsePayload)
148 }
149 }
150
151 // public sendRequest (
152 // uuid: `${string}-${string}-${string}-${string}-${string}`,
153 // procedureName: ProcedureName,
154 // requestPayload: RequestPayload
155 // ): void {
156 // this.uiServer.sendRequest(
157 // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
158 // )
159 // }
160
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))
167 }
168 }
169
170 public logPrefix = (modName: string, methodName: string): string => {
171 return this.uiServer.logPrefix(modName, methodName, this.version)
172 }
173
174 public deleteBroadcastChannelRequest (
175 uuid: `${string}-${string}-${string}-${string}-${string}`
176 ): void {
177 this.broadcastChannelRequests.delete(uuid)
178 }
179
180 public getBroadcastChannelExpectedResponses (
181 uuid: `${string}-${string}-${string}-${string}-${string}`
182 ): number {
183 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
184 return this.broadcastChannelRequests.get(uuid)!
185 }
186
187 protected handleProtocolRequest (
188 uuid: `${string}-${string}-${string}-${string}-${string}`,
189 procedureName: ProcedureName,
190 payload: RequestPayload
191 ): void {
192 this.sendBroadcastChannelRequest(
193 uuid,
194 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
195 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
196 payload
197 )
198 }
199
200 private sendBroadcastChannelRequest (
201 uuid: `${string}-${string}-${string}-${string}-${string}`,
202 procedureName: BroadcastChannelProcedureName,
203 payload: BroadcastChannelRequestPayload
204 ): void {
205 if (isNotEmptyArray(payload.hashIds)) {
206 payload.hashIds = payload.hashIds
207 .map(hashId => {
208 if (this.uiServer.chargingStations.has(hashId)) {
209 return hashId
210 }
211 logger.warn(
212 `${this.logPrefix(
213 moduleName,
214 'sendBroadcastChannelRequest'
215 )} Charging station with hashId '${hashId}' not found`
216 )
217 return undefined
218 })
219 .filter(hashId => hashId != null) as string[]
220 } else {
221 delete payload.hashIds
222 }
223 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
224 ? payload.hashIds.length
225 : this.uiServer.chargingStations.size
226 if (expectedNumberOfResponses === 0) {
227 throw new BaseError(
228 'hashIds array in the request payload does not contain any valid charging station hashId'
229 )
230 }
231 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
232 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
233 }
234
235 private handleListTemplates (): ResponsePayload {
236 return {
237 status: ResponseStatus.SUCCESS,
238 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
239 } satisfies ResponsePayload
240 }
241
242 private handleListChargingStations (): ResponsePayload {
243 return {
244 status: ResponseStatus.SUCCESS,
245 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
246 } satisfies ResponsePayload
247 }
248
249 private async handleAddChargingStations (
250 _uuid?: `${string}-${string}-${string}-${string}-${string}`,
251 _procedureName?: ProcedureName,
252 requestPayload?: RequestPayload
253 ): Promise<ResponsePayload> {
254 const { template, numberOfStations, options } = requestPayload as {
255 template: string
256 numberOfStations: number
257 options?: ChargingStationOptions
258 }
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 }
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,
282 `${template}.json`,
283 options
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
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
326 private handleSimulatorState (): ResponsePayload {
327 try {
328 return {
329 status: ResponseStatus.SUCCESS,
330 state: Bootstrap.getInstance().getState() as unknown as JsonObject
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
341 private async handleStartSimulator (): Promise<ResponsePayload> {
342 try {
343 await Bootstrap.getInstance().start()
344 return { status: ResponseStatus.SUCCESS }
345 } catch (error) {
346 return {
347 status: ResponseStatus.FAILURE,
348 errorMessage: (error as Error).message,
349 errorStack: (error as Error).stack
350 } satisfies ResponsePayload
351 }
352 }
353
354 private async handleStopSimulator (): Promise<ResponsePayload> {
355 try {
356 await Bootstrap.getInstance().stop()
357 return { status: ResponseStatus.SUCCESS }
358 } catch (error) {
359 return {
360 status: ResponseStatus.FAILURE,
361 errorMessage: (error as Error).message,
362 errorStack: (error as Error).stack
363 } satisfies ResponsePayload
364 }
365 }
366 }