feat: add addChargingStations command to UI API
[e-mobility-charging-stations-simulator.git] / src / charging-station / ui-server / ui-services / AbstractUIService.ts
CommitLineData
66a7748d 1import { BaseError, type OCPPError } from '../../../exception/index.js'
675fa8e3 2import {
268a74bb
JB
3 BroadcastChannelProcedureName,
4 type BroadcastChannelRequestPayload,
66a7748d 5 type JsonType,
32de5a57 6 ProcedureName,
976d11ec
JB
7 type ProtocolRequest,
8 type ProtocolRequestHandler,
f130b8e6 9 type ProtocolResponse,
976d11ec
JB
10 type ProtocolVersion,
11 type RequestPayload,
12 type ResponsePayload,
66a7748d
JB
13 ResponseStatus
14} from '../../../types/index.js'
4b9332af 15import { isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
66a7748d
JB
16import { Bootstrap } from '../../Bootstrap.js'
17import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
18import type { AbstractUIServer } from '../AbstractUIServer.js'
4198ad5c 19
66a7748d 20const moduleName = 'AbstractUIService'
32de5a57 21
268a74bb 22export abstract class AbstractUIService {
a37fc6dc 23 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
66a7748d
JB
24 ProcedureName,
25 BroadcastChannelProcedureName
a37fc6dc
JB
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,
66a7748d 33 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
a37fc6dc
JB
34 ],
35 [
36 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d 37 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
a37fc6dc
JB
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,
66a7748d 50 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
a37fc6dc
JB
51 ],
52 [
53 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
66a7748d
JB
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
02a6943a 67 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
42e341c4 68 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
32de5a57 69 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
c5ecc04d 70 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
32de5a57 71 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
66a7748d
JB
72 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
73 ])
74 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
75 this.broadcastChannelRequests = new Map<string, number>()
4198ad5c
JB
76 }
77
66a7748d
JB
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
32de5a57 83 try {
66a7748d 84 [messageId, command, requestPayload] = request
32de5a57 85
66a7748d 86 if (!this.requestHandlers.has(command)) {
32de5a57
LM
87 throw new BaseError(
88 `${command} is not implemented to handle message payload ${JSON.stringify(
89 requestPayload,
4ed03b6e 90 undefined,
66a7748d
JB
91 2
92 )}`
93 )
4198ad5c 94 }
89b7a234 95
6c8f5d90 96 // Call the request handler to build the response payload
66a7748d 97 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4b9332af
JB
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 }
32de5a57
LM
110 } catch (error) {
111 // Log
66a7748d 112 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
6c8f5d90 113 responsePayload = {
551e477c 114 hashIds: requestPayload?.hashIds,
6c8f5d90
JB
115 status: ResponseStatus.FAILURE,
116 command,
117 requestPayload,
118 responsePayload,
7375968c
JB
119 errorMessage: (error as OCPPError).message,
120 errorStack: (error as OCPPError).stack,
66a7748d 121 errorDetails: (error as OCPPError).details
c5ecc04d 122 } satisfies ResponsePayload
f130b8e6 123 }
aa63c9b7 124 if (responsePayload != null) {
66a7748d 125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
aa63c9b7 126 return this.uiServer.buildProtocolResponse(messageId!, responsePayload)
4198ad5c 127 }
6c8f5d90
JB
128 }
129
66a7748d 130 // public sendRequest (
0b22144c
JB
131 // messageId: string,
132 // procedureName: ProcedureName,
66a7748d 133 // requestPayload: RequestPayload
0b22144c
JB
134 // ): void {
135 // this.uiServer.sendRequest(
66a7748d
JB
136 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
137 // )
0b22144c 138 // }
32de5a57 139
66a7748d 140 public sendResponse (messageId: string, responsePayload: ResponsePayload): void {
1ca4a038 141 if (this.uiServer.hasResponseHandler(messageId)) {
66a7748d 142 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload))
1ca4a038 143 }
4198ad5c
JB
144 }
145
8b7072dc 146 public logPrefix = (modName: string, methodName: string): string => {
66a7748d
JB
147 return this.uiServer.logPrefix(modName, methodName, this.version)
148 }
0d2cec76 149
66a7748d
JB
150 public deleteBroadcastChannelRequest (uuid: string): void {
151 this.broadcastChannelRequests.delete(uuid)
0d2cec76
JB
152 }
153
66a7748d
JB
154 public getBroadcastChannelExpectedResponses (uuid: string): number {
155 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
156 return this.broadcastChannelRequests.get(uuid)!
0d2cec76
JB
157 }
158
66a7748d 159 protected handleProtocolRequest (
2a3cf7fc
JB
160 uuid: string,
161 procedureName: ProcedureName,
66a7748d 162 payload: RequestPayload
2a3cf7fc
JB
163 ): void {
164 this.sendBroadcastChannelRequest(
165 uuid,
66a7748d 166 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
a37fc6dc 167 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
66a7748d
JB
168 payload
169 )
2a3cf7fc
JB
170 }
171
66a7748d 172 private sendBroadcastChannelRequest (
0d2cec76
JB
173 uuid: string,
174 procedureName: BroadcastChannelProcedureName,
66a7748d 175 payload: BroadcastChannelRequestPayload
0d2cec76 176 ): void {
9bf0ef23 177 if (isNotEmptyArray(payload.hashIds)) {
0d2cec76 178 payload.hashIds = payload.hashIds
5dc7c990 179 .map(hashId => {
5199f9fd 180 if (this.uiServer.chargingStations.has(hashId)) {
66a7748d 181 return hashId
0d2cec76 182 }
3a6ef20a
JB
183 logger.warn(
184 `${this.logPrefix(
185 moduleName,
66a7748d
JB
186 'sendBroadcastChannelRequest'
187 )} Charging station with hashId '${hashId}' not found`
188 )
189 return undefined
f12cf7ef 190 })
a974c8e4 191 .filter(hashId => hashId != null) as string[]
3a6ef20a 192 } else {
66a7748d 193 delete payload.hashIds
0d2cec76 194 }
3a6ef20a
JB
195 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
196 ? payload.hashIds.length
66a7748d 197 : this.uiServer.chargingStations.size
3a6ef20a
JB
198 if (expectedNumberOfResponses === 0) {
199 throw new BaseError(
66a7748d
JB
200 'hashIds array in the request payload does not contain any valid charging station hashId'
201 )
3a6ef20a 202 }
66a7748d
JB
203 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
204 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
6c8f5d90
JB
205 }
206
42e341c4
JB
207 private handleListTemplates (): ResponsePayload {
208 return {
209 status: ResponseStatus.SUCCESS,
210 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
211 } satisfies ResponsePayload
212 }
213
66a7748d 214 private handleListChargingStations (): ResponsePayload {
32de5a57
LM
215 return {
216 status: ResponseStatus.SUCCESS,
66a7748d
JB
217 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
218 } satisfies ResponsePayload
32de5a57
LM
219 }
220
c5ecc04d
JB
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
66a7748d 255 private async handleStartSimulator (): Promise<ResponsePayload> {
4ec634b7 256 try {
66a7748d
JB
257 await Bootstrap.getInstance().start()
258 return { status: ResponseStatus.SUCCESS }
9c0ecbdf 259 } catch {
66a7748d 260 return { status: ResponseStatus.FAILURE }
4ec634b7 261 }
32de5a57
LM
262 }
263
66a7748d 264 private async handleStopSimulator (): Promise<ResponsePayload> {
4ec634b7 265 try {
66a7748d
JB
266 await Bootstrap.getInstance().stop()
267 return { status: ResponseStatus.SUCCESS }
9c0ecbdf 268 } catch {
66a7748d 269 return { status: ResponseStatus.FAILURE }
4ec634b7 270 }
4198ad5c
JB
271 }
272}