feat: add performance statistics to 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 ConfigurationSection,
6 type JsonType,
7 ProcedureName,
8 type ProtocolRequest,
9 type ProtocolRequestHandler,
10 type ProtocolResponse,
11 type ProtocolVersion,
12 type RequestPayload,
13 type ResponsePayload,
14 ResponseStatus,
15 type StorageConfiguration
16 } from '../../../types/index.js'
17 import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
18 import { Bootstrap } from '../../Bootstrap.js'
19 import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
20 import type { AbstractUIServer } from '../AbstractUIServer.js'
21
22 const moduleName = 'AbstractUIService'
23
24 export abstract class AbstractUIService {
25 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
26 ProcedureName,
27 BroadcastChannelProcedureName
28 >([
29 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
30 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
31 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
32 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
33 [
34 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
35 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
36 ],
37 [
38 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
39 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
40 ],
41 [ProcedureName.SET_SUPERVISION_URL, BroadcastChannelProcedureName.SET_SUPERVISION_URL],
42 [ProcedureName.START_TRANSACTION, BroadcastChannelProcedureName.START_TRANSACTION],
43 [ProcedureName.STOP_TRANSACTION, BroadcastChannelProcedureName.STOP_TRANSACTION],
44 [ProcedureName.AUTHORIZE, BroadcastChannelProcedureName.AUTHORIZE],
45 [ProcedureName.BOOT_NOTIFICATION, BroadcastChannelProcedureName.BOOT_NOTIFICATION],
46 [ProcedureName.STATUS_NOTIFICATION, BroadcastChannelProcedureName.STATUS_NOTIFICATION],
47 [ProcedureName.HEARTBEAT, BroadcastChannelProcedureName.HEARTBEAT],
48 [ProcedureName.METER_VALUES, BroadcastChannelProcedureName.METER_VALUES],
49 [ProcedureName.DATA_TRANSFER, BroadcastChannelProcedureName.DATA_TRANSFER],
50 [
51 ProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
52 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
53 ],
54 [
55 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
56 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION
57 ]
58 ])
59
60 protected readonly requestHandlers: Map<ProcedureName, ProtocolRequestHandler>
61 private readonly version: ProtocolVersion
62 private readonly uiServer: AbstractUIServer
63 private readonly uiServiceWorkerBroadcastChannel: UIServiceWorkerBroadcastChannel
64 private readonly broadcastChannelRequests: Map<string, number>
65
66 constructor (uiServer: AbstractUIServer, version: ProtocolVersion) {
67 this.uiServer = uiServer
68 this.version = version
69 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
70 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
71 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
72 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
73 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
74 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
75 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
76 ])
77 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
78 this.broadcastChannelRequests = new Map<string, number>()
79 }
80
81 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
82 let messageId: string | undefined
83 let command: ProcedureName | undefined
84 let requestPayload: RequestPayload | undefined
85 let responsePayload: ResponsePayload | undefined
86 try {
87 [messageId, command, requestPayload] = request
88
89 if (!this.requestHandlers.has(command)) {
90 throw new BaseError(
91 `${command} is not implemented to handle message payload ${JSON.stringify(
92 requestPayload,
93 undefined,
94 2
95 )}`
96 )
97 }
98
99 // Call the request handler to build the response payload
100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101 const requestHandler = this.requestHandlers.get(command)!
102 if (isAsyncFunction(requestHandler)) {
103 responsePayload = await requestHandler(messageId, command, requestPayload)
104 } else {
105 responsePayload = (
106 requestHandler as (
107 uuid?: string,
108 procedureName?: ProcedureName,
109 payload?: RequestPayload
110 ) => undefined | ResponsePayload
111 )(messageId, command, requestPayload)
112 }
113 } catch (error) {
114 // Log
115 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
116 responsePayload = {
117 hashIds: requestPayload?.hashIds,
118 status: ResponseStatus.FAILURE,
119 command,
120 requestPayload,
121 responsePayload,
122 errorMessage: (error as OCPPError).message,
123 errorStack: (error as OCPPError).stack,
124 errorDetails: (error as OCPPError).details
125 } satisfies ResponsePayload
126 }
127 if (responsePayload != null) {
128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129 return this.uiServer.buildProtocolResponse(messageId!, responsePayload)
130 }
131 }
132
133 // public sendRequest (
134 // messageId: string,
135 // procedureName: ProcedureName,
136 // requestPayload: RequestPayload
137 // ): void {
138 // this.uiServer.sendRequest(
139 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
140 // )
141 // }
142
143 public sendResponse (messageId: string, responsePayload: ResponsePayload): void {
144 if (this.uiServer.hasResponseHandler(messageId)) {
145 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload))
146 }
147 }
148
149 public logPrefix = (modName: string, methodName: string): string => {
150 return this.uiServer.logPrefix(modName, methodName, this.version)
151 }
152
153 public deleteBroadcastChannelRequest (uuid: string): void {
154 this.broadcastChannelRequests.delete(uuid)
155 }
156
157 public getBroadcastChannelExpectedResponses (uuid: string): number {
158 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159 return this.broadcastChannelRequests.get(uuid)!
160 }
161
162 protected handleProtocolRequest (
163 uuid: string,
164 procedureName: ProcedureName,
165 payload: RequestPayload
166 ): void {
167 this.sendBroadcastChannelRequest(
168 uuid,
169 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
170 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
171 payload
172 )
173 }
174
175 private sendBroadcastChannelRequest (
176 uuid: string,
177 procedureName: BroadcastChannelProcedureName,
178 payload: BroadcastChannelRequestPayload
179 ): void {
180 if (isNotEmptyArray(payload.hashIds)) {
181 payload.hashIds = payload.hashIds
182 .map(hashId => {
183 if (this.uiServer.chargingStations.has(hashId)) {
184 return hashId
185 }
186 logger.warn(
187 `${this.logPrefix(
188 moduleName,
189 'sendBroadcastChannelRequest'
190 )} Charging station with hashId '${hashId}' not found`
191 )
192 return undefined
193 })
194 .filter(hashId => hashId != null) as string[]
195 } else {
196 delete payload.hashIds
197 }
198 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
199 ? payload.hashIds.length
200 : this.uiServer.chargingStations.size
201 if (expectedNumberOfResponses === 0) {
202 throw new BaseError(
203 'hashIds array in the request payload does not contain any valid charging station hashId'
204 )
205 }
206 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
207 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
208 }
209
210 private handleListTemplates (): ResponsePayload {
211 return {
212 status: ResponseStatus.SUCCESS,
213 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
214 } satisfies ResponsePayload
215 }
216
217 private handleListChargingStations (): ResponsePayload {
218 return {
219 status: ResponseStatus.SUCCESS,
220 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
221 } satisfies ResponsePayload
222 }
223
224 private async handleAddChargingStations (
225 messageId?: string,
226 procedureName?: ProcedureName,
227 requestPayload?: RequestPayload
228 ): Promise<ResponsePayload> {
229 const { template, numberOfStations } = requestPayload as {
230 template: string
231 numberOfStations: number
232 }
233 if (!this.uiServer.chargingStationTemplates.has(template)) {
234 return {
235 status: ResponseStatus.FAILURE,
236 errorMessage: `Template '${template}' not found`
237 } satisfies ResponsePayload
238 }
239 for (let i = 0; i < numberOfStations; i++) {
240 try {
241 await Bootstrap.getInstance().addChargingStation(
242 Bootstrap.getInstance().getLastIndex(template) + 1,
243 `${template}.json`
244 )
245 } catch (error) {
246 return {
247 status: ResponseStatus.FAILURE,
248 errorMessage: (error as Error).message,
249 errorStack: (error as Error).stack
250 } satisfies ResponsePayload
251 }
252 }
253 return {
254 status: ResponseStatus.SUCCESS
255 }
256 }
257
258 private handlePerformanceStatistics (): ResponsePayload {
259 if (
260 Configuration.getConfigurationSection<StorageConfiguration>(
261 ConfigurationSection.performanceStorage
262 ).enabled !== true
263 ) {
264 return {
265 status: ResponseStatus.FAILURE,
266 errorMessage: 'Performance statistics storage is not enabled'
267 } satisfies ResponsePayload
268 }
269 try {
270 return {
271 status: ResponseStatus.SUCCESS,
272 performanceStatistics: [
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 ...Bootstrap.getInstance().getPerformanceStatistics()!
275 ] as JsonType[]
276 }
277 } catch (error) {
278 return {
279 status: ResponseStatus.FAILURE,
280 errorMessage: (error as Error).message,
281 errorStack: (error as Error).stack
282 } satisfies ResponsePayload
283 }
284 }
285
286 private async handleStartSimulator (): Promise<ResponsePayload> {
287 try {
288 await Bootstrap.getInstance().start()
289 return { status: ResponseStatus.SUCCESS }
290 } catch (error) {
291 return {
292 status: ResponseStatus.FAILURE,
293 errorMessage: (error as Error).message,
294 errorStack: (error as Error).stack
295 } satisfies ResponsePayload
296 }
297 }
298
299 private async handleStopSimulator (): Promise<ResponsePayload> {
300 try {
301 await Bootstrap.getInstance().stop()
302 return { status: ResponseStatus.SUCCESS }
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 }