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