refactor: add sanity checks at adding charging station(s)
[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<string, number>
70
71 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
72 this.uiServer = uiServer
73 this.version = version
74 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
75 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
76 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
77 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
78 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
79 [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
80 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
81 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
82 ])
83 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
84 this.broadcastChannelRequests = new Map<string, number>()
85 }
86
87 public stop (): void {
88 this.broadcastChannelRequests.clear()
89 this.uiServiceWorkerBroadcastChannel.close()
90 }
91
92 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
93 let messageId: string | undefined
94 let command: ProcedureName | undefined
95 let requestPayload: RequestPayload | undefined
96 let responsePayload: ResponsePayload | undefined
97 try {
98 [messageId, command, requestPayload] = request
99
100 if (!this.requestHandlers.has(command)) {
101 throw new BaseError(
102 `'${command}' is not implemented to handle message payload ${JSON.stringify(
103 requestPayload,
104 undefined,
105 2
106 )}`
107 )
108 }
109
110 // Call the request handler to build the response payload
111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
112 const requestHandler = this.requestHandlers.get(command)!
113 if (isAsyncFunction(requestHandler)) {
114 responsePayload = await requestHandler(messageId, command, requestPayload)
115 } else {
116 responsePayload = (
117 requestHandler as (
118 uuid?: string,
119 procedureName?: ProcedureName,
120 payload?: RequestPayload
121 ) => undefined | ResponsePayload
122 )(messageId, command, requestPayload)
123 }
124 } catch (error) {
125 // Log
126 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
127 responsePayload = {
128 hashIds: requestPayload?.hashIds,
129 status: ResponseStatus.FAILURE,
130 command,
131 requestPayload,
132 responsePayload,
133 errorMessage: (error as OCPPError).message,
134 errorStack: (error as OCPPError).stack,
135 errorDetails: (error as OCPPError).details
136 } satisfies ResponsePayload
137 }
138 if (responsePayload != null) {
139 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
140 return this.uiServer.buildProtocolResponse(messageId!, responsePayload)
141 }
142 }
143
144 // public sendRequest (
145 // messageId: string,
146 // procedureName: ProcedureName,
147 // requestPayload: RequestPayload
148 // ): void {
149 // this.uiServer.sendRequest(
150 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
151 // )
152 // }
153
154 public sendResponse (messageId: string, responsePayload: ResponsePayload): void {
155 if (this.uiServer.hasResponseHandler(messageId)) {
156 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload))
157 }
158 }
159
160 public logPrefix = (modName: string, methodName: string): string => {
161 return this.uiServer.logPrefix(modName, methodName, this.version)
162 }
163
164 public deleteBroadcastChannelRequest (uuid: string): void {
165 this.broadcastChannelRequests.delete(uuid)
166 }
167
168 public getBroadcastChannelExpectedResponses (uuid: string): number {
169 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
170 return this.broadcastChannelRequests.get(uuid)!
171 }
172
173 protected handleProtocolRequest (
174 uuid: string,
175 procedureName: ProcedureName,
176 payload: RequestPayload
177 ): void {
178 this.sendBroadcastChannelRequest(
179 uuid,
180 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
181 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
182 payload
183 )
184 }
185
186 private sendBroadcastChannelRequest (
187 uuid: string,
188 procedureName: BroadcastChannelProcedureName,
189 payload: BroadcastChannelRequestPayload
190 ): void {
191 if (isNotEmptyArray(payload.hashIds)) {
192 payload.hashIds = payload.hashIds
193 .map(hashId => {
194 if (this.uiServer.chargingStations.has(hashId)) {
195 return hashId
196 }
197 logger.warn(
198 `${this.logPrefix(
199 moduleName,
200 'sendBroadcastChannelRequest'
201 )} Charging station with hashId '${hashId}' not found`
202 )
203 return undefined
204 })
205 .filter(hashId => hashId != null) as string[]
206 } else {
207 delete payload.hashIds
208 }
209 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
210 ? payload.hashIds.length
211 : this.uiServer.chargingStations.size
212 if (expectedNumberOfResponses === 0) {
213 throw new BaseError(
214 'hashIds array in the request payload does not contain any valid charging station hashId'
215 )
216 }
217 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
218 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
219 }
220
221 private handleListTemplates (): ResponsePayload {
222 return {
223 status: ResponseStatus.SUCCESS,
224 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
225 } satisfies ResponsePayload
226 }
227
228 private handleListChargingStations (): ResponsePayload {
229 return {
230 status: ResponseStatus.SUCCESS,
231 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
232 } satisfies ResponsePayload
233 }
234
235 private async handleAddChargingStations (
236 _messageId?: string,
237 _procedureName?: ProcedureName,
238 requestPayload?: RequestPayload
239 ): Promise<ResponsePayload> {
240 const { template, numberOfStations, options } = requestPayload as {
241 template: string
242 numberOfStations: number
243 options?: ChargingStationOptions
244 }
245 if (!Bootstrap.getInstance().getState().started) {
246 return {
247 status: ResponseStatus.FAILURE,
248 errorMessage:
249 'Cannot add charging station(s) while the charging stations simulator is not started'
250 } satisfies ResponsePayload
251 }
252 if (typeof template !== 'string' || typeof numberOfStations !== 'number') {
253 return {
254 status: ResponseStatus.FAILURE,
255 errorMessage: 'Invalid request payload'
256 } satisfies ResponsePayload
257 }
258 if (!this.uiServer.chargingStationTemplates.has(template)) {
259 return {
260 status: ResponseStatus.FAILURE,
261 errorMessage: `Template '${template}' not found`
262 } satisfies ResponsePayload
263 }
264 for (let i = 0; i < numberOfStations; i++) {
265 try {
266 await Bootstrap.getInstance().addChargingStation(
267 Bootstrap.getInstance().getLastIndex(template) + 1,
268 `${template}.json`,
269 options
270 )
271 } catch (error) {
272 return {
273 status: ResponseStatus.FAILURE,
274 errorMessage: (error as Error).message,
275 errorStack: (error as Error).stack
276 } satisfies ResponsePayload
277 }
278 }
279 return {
280 status: ResponseStatus.SUCCESS
281 }
282 }
283
284 private handlePerformanceStatistics (): ResponsePayload {
285 if (
286 Configuration.getConfigurationSection<StorageConfiguration>(
287 ConfigurationSection.performanceStorage
288 ).enabled !== true
289 ) {
290 return {
291 status: ResponseStatus.FAILURE,
292 errorMessage: 'Performance statistics storage is not enabled'
293 } satisfies ResponsePayload
294 }
295 try {
296 return {
297 status: ResponseStatus.SUCCESS,
298 performanceStatistics: [
299 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
300 ...Bootstrap.getInstance().getPerformanceStatistics()!
301 ] as JsonType[]
302 }
303 } catch (error) {
304 return {
305 status: ResponseStatus.FAILURE,
306 errorMessage: (error as Error).message,
307 errorStack: (error as Error).stack
308 } satisfies ResponsePayload
309 }
310 }
311
312 private handleSimulatorState (): ResponsePayload {
313 try {
314 return {
315 status: ResponseStatus.SUCCESS,
316 state: Bootstrap.getInstance().getState()
317 } satisfies ResponsePayload
318 } catch (error) {
319 return {
320 status: ResponseStatus.FAILURE,
321 errorMessage: (error as Error).message,
322 errorStack: (error as Error).stack
323 } satisfies ResponsePayload
324 }
325 }
326
327 private async handleStartSimulator (): Promise<ResponsePayload> {
328 try {
329 await Bootstrap.getInstance().start()
330 return { status: ResponseStatus.SUCCESS }
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 handleStopSimulator (): Promise<ResponsePayload> {
341 try {
342 await Bootstrap.getInstance().stop()
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 }