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