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