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