refactor(ui): use watchers to refresh display
[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,
71ac2bd7 5 type ChargingStationOptions,
a66bbcfe 6 ConfigurationSection,
66a7748d 7 type JsonType,
32de5a57 8 ProcedureName,
976d11ec
JB
9 type ProtocolRequest,
10 type ProtocolRequestHandler,
f130b8e6 11 type ProtocolResponse,
976d11ec
JB
12 type ProtocolVersion,
13 type RequestPayload,
14 type ResponsePayload,
a66bbcfe
JB
15 ResponseStatus,
16 type StorageConfiguration
66a7748d 17} from '../../../types/index.js'
a66bbcfe 18import { Configuration, isAsyncFunction, isNotEmptyArray, logger } from '../../../utils/index.js'
66a7748d
JB
19import { Bootstrap } from '../../Bootstrap.js'
20import { UIServiceWorkerBroadcastChannel } from '../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
21import type { AbstractUIServer } from '../AbstractUIServer.js'
4198ad5c 22
66a7748d 23const moduleName = 'AbstractUIService'
32de5a57 24
268a74bb 25export abstract class AbstractUIService {
a37fc6dc 26 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping = new Map<
66a7748d
JB
27 ProcedureName,
28 BroadcastChannelProcedureName
a37fc6dc
JB
29 >([
30 [ProcedureName.START_CHARGING_STATION, BroadcastChannelProcedureName.START_CHARGING_STATION],
31 [ProcedureName.STOP_CHARGING_STATION, BroadcastChannelProcedureName.STOP_CHARGING_STATION],
09e5a7a8
JB
32 [
33 ProcedureName.DELETE_CHARGING_STATIONS,
34 BroadcastChannelProcedureName.DELETE_CHARGING_STATIONS
35 ],
a37fc6dc
JB
36 [ProcedureName.CLOSE_CONNECTION, BroadcastChannelProcedureName.CLOSE_CONNECTION],
37 [ProcedureName.OPEN_CONNECTION, BroadcastChannelProcedureName.OPEN_CONNECTION],
38 [
39 ProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d 40 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR
a37fc6dc
JB
41 ],
42 [
43 ProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
66a7748d 44 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
a37fc6dc
JB
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,
66a7748d 57 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION
a37fc6dc
JB
58 ],
59 [
60 ProcedureName.FIRMWARE_STATUS_NOTIFICATION,
66a7748d
JB
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
02a6943a 74 this.requestHandlers = new Map<ProcedureName, ProtocolRequestHandler>([
42e341c4 75 [ProcedureName.LIST_TEMPLATES, this.handleListTemplates.bind(this)],
32de5a57 76 [ProcedureName.LIST_CHARGING_STATIONS, this.handleListChargingStations.bind(this)],
c5ecc04d 77 [ProcedureName.ADD_CHARGING_STATIONS, this.handleAddChargingStations.bind(this)],
a66bbcfe 78 [ProcedureName.PERFORMANCE_STATISTICS, this.handlePerformanceStatistics.bind(this)],
240fa4da 79 [ProcedureName.SIMULATOR_STATE, this.handleSimulatorState.bind(this)],
32de5a57 80 [ProcedureName.START_SIMULATOR, this.handleStartSimulator.bind(this)],
66a7748d
JB
81 [ProcedureName.STOP_SIMULATOR, this.handleStopSimulator.bind(this)]
82 ])
83 this.uiServiceWorkerBroadcastChannel = new UIServiceWorkerBroadcastChannel(this)
84 this.broadcastChannelRequests = new Map<string, number>()
4198ad5c
JB
85 }
86
09e5a7a8
JB
87 public stop (): void {
88 this.broadcastChannelRequests.clear()
89 this.uiServiceWorkerBroadcastChannel.close()
90 }
91
66a7748d
JB
92 public async requestHandler (request: ProtocolRequest): Promise<ProtocolResponse | undefined> {
93 let messageId: string | undefined
94 let command: ProcedureName | undefined
95 let requestPayload: RequestPayload | undefined
96 let responsePayload: ResponsePayload | undefined
32de5a57 97 try {
66a7748d 98 [messageId, command, requestPayload] = request
32de5a57 99
66a7748d 100 if (!this.requestHandlers.has(command)) {
32de5a57 101 throw new BaseError(
240fa4da 102 `'${command}' is not implemented to handle message payload ${JSON.stringify(
32de5a57 103 requestPayload,
4ed03b6e 104 undefined,
66a7748d
JB
105 2
106 )}`
107 )
4198ad5c 108 }
89b7a234 109
6c8f5d90 110 // Call the request handler to build the response payload
66a7748d 111 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
4b9332af
JB
112 const requestHandler = this.requestHandlers.get(command)!
113 if (isAsyncFunction(requestHandler)) {
114 responsePayload = await requestHandler(messageId, command, requestPayload)
115 } else {
116 responsePayload = (
117 requestHandler as (
118 uuid?: string,
119 procedureName?: ProcedureName,
120 payload?: RequestPayload
121 ) => undefined | ResponsePayload
122 )(messageId, command, requestPayload)
123 }
32de5a57
LM
124 } catch (error) {
125 // Log
66a7748d 126 logger.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error)
6c8f5d90 127 responsePayload = {
551e477c 128 hashIds: requestPayload?.hashIds,
6c8f5d90
JB
129 status: ResponseStatus.FAILURE,
130 command,
131 requestPayload,
132 responsePayload,
7375968c
JB
133 errorMessage: (error as OCPPError).message,
134 errorStack: (error as OCPPError).stack,
66a7748d 135 errorDetails: (error as OCPPError).details
c5ecc04d 136 } satisfies ResponsePayload
f130b8e6 137 }
aa63c9b7 138 if (responsePayload != null) {
66a7748d 139 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
aa63c9b7 140 return this.uiServer.buildProtocolResponse(messageId!, responsePayload)
4198ad5c 141 }
6c8f5d90
JB
142 }
143
66a7748d 144 // public sendRequest (
0b22144c
JB
145 // messageId: string,
146 // procedureName: ProcedureName,
66a7748d 147 // requestPayload: RequestPayload
0b22144c
JB
148 // ): void {
149 // this.uiServer.sendRequest(
66a7748d
JB
150 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
151 // )
0b22144c 152 // }
32de5a57 153
66a7748d 154 public sendResponse (messageId: string, responsePayload: ResponsePayload): void {
1ca4a038 155 if (this.uiServer.hasResponseHandler(messageId)) {
66a7748d 156 this.uiServer.sendResponse(this.uiServer.buildProtocolResponse(messageId, responsePayload))
1ca4a038 157 }
4198ad5c
JB
158 }
159
8b7072dc 160 public logPrefix = (modName: string, methodName: string): string => {
66a7748d
JB
161 return this.uiServer.logPrefix(modName, methodName, this.version)
162 }
0d2cec76 163
66a7748d
JB
164 public deleteBroadcastChannelRequest (uuid: string): void {
165 this.broadcastChannelRequests.delete(uuid)
0d2cec76
JB
166 }
167
66a7748d
JB
168 public getBroadcastChannelExpectedResponses (uuid: string): number {
169 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
170 return this.broadcastChannelRequests.get(uuid)!
0d2cec76
JB
171 }
172
66a7748d 173 protected handleProtocolRequest (
2a3cf7fc
JB
174 uuid: string,
175 procedureName: ProcedureName,
66a7748d 176 payload: RequestPayload
2a3cf7fc
JB
177 ): void {
178 this.sendBroadcastChannelRequest(
179 uuid,
66a7748d 180 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
a37fc6dc 181 AbstractUIService.ProcedureNameToBroadCastChannelProcedureNameMapping.get(procedureName)!,
66a7748d
JB
182 payload
183 )
2a3cf7fc
JB
184 }
185
66a7748d 186 private sendBroadcastChannelRequest (
0d2cec76
JB
187 uuid: string,
188 procedureName: BroadcastChannelProcedureName,
66a7748d 189 payload: BroadcastChannelRequestPayload
0d2cec76 190 ): void {
9bf0ef23 191 if (isNotEmptyArray(payload.hashIds)) {
0d2cec76 192 payload.hashIds = payload.hashIds
5dc7c990 193 .map(hashId => {
5199f9fd 194 if (this.uiServer.chargingStations.has(hashId)) {
66a7748d 195 return hashId
0d2cec76 196 }
3a6ef20a
JB
197 logger.warn(
198 `${this.logPrefix(
199 moduleName,
66a7748d
JB
200 'sendBroadcastChannelRequest'
201 )} Charging station with hashId '${hashId}' not found`
202 )
203 return undefined
f12cf7ef 204 })
a974c8e4 205 .filter(hashId => hashId != null) as string[]
3a6ef20a 206 } else {
66a7748d 207 delete payload.hashIds
0d2cec76 208 }
3a6ef20a
JB
209 const expectedNumberOfResponses = Array.isArray(payload.hashIds)
210 ? payload.hashIds.length
66a7748d 211 : this.uiServer.chargingStations.size
3a6ef20a
JB
212 if (expectedNumberOfResponses === 0) {
213 throw new BaseError(
66a7748d
JB
214 'hashIds array in the request payload does not contain any valid charging station hashId'
215 )
3a6ef20a 216 }
66a7748d
JB
217 this.uiServiceWorkerBroadcastChannel.sendRequest([uuid, procedureName, payload])
218 this.broadcastChannelRequests.set(uuid, expectedNumberOfResponses)
6c8f5d90
JB
219 }
220
42e341c4
JB
221 private handleListTemplates (): ResponsePayload {
222 return {
223 status: ResponseStatus.SUCCESS,
224 templates: [...this.uiServer.chargingStationTemplates.values()] as JsonType[]
225 } satisfies ResponsePayload
226 }
227
66a7748d 228 private handleListChargingStations (): ResponsePayload {
32de5a57
LM
229 return {
230 status: ResponseStatus.SUCCESS,
66a7748d
JB
231 chargingStations: [...this.uiServer.chargingStations.values()] as JsonType[]
232 } satisfies ResponsePayload
32de5a57
LM
233 }
234
c5ecc04d 235 private async handleAddChargingStations (
0447d90f
JB
236 _messageId?: string,
237 _procedureName?: ProcedureName,
c5ecc04d
JB
238 requestPayload?: RequestPayload
239 ): Promise<ResponsePayload> {
71ac2bd7 240 const { template, numberOfStations, options } = requestPayload as {
c5ecc04d
JB
241 template: string
242 numberOfStations: number
71ac2bd7 243 options?: ChargingStationOptions
c5ecc04d
JB
244 }
245 if (!this.uiServer.chargingStationTemplates.has(template)) {
246 return {
247 status: ResponseStatus.FAILURE,
248 errorMessage: `Template '${template}' not found`
249 } satisfies ResponsePayload
250 }
251 for (let i = 0; i < numberOfStations; i++) {
252 try {
253 await Bootstrap.getInstance().addChargingStation(
254 Bootstrap.getInstance().getLastIndex(template) + 1,
71ac2bd7
JB
255 `${template}.json`,
256 options
c5ecc04d
JB
257 )
258 } catch (error) {
259 return {
260 status: ResponseStatus.FAILURE,
261 errorMessage: (error as Error).message,
262 errorStack: (error as Error).stack
263 } satisfies ResponsePayload
264 }
265 }
266 return {
267 status: ResponseStatus.SUCCESS
268 }
269 }
270
a66bbcfe
JB
271 private handlePerformanceStatistics (): ResponsePayload {
272 if (
273 Configuration.getConfigurationSection<StorageConfiguration>(
274 ConfigurationSection.performanceStorage
275 ).enabled !== true
276 ) {
277 return {
278 status: ResponseStatus.FAILURE,
279 errorMessage: 'Performance statistics storage is not enabled'
280 } satisfies ResponsePayload
281 }
282 try {
283 return {
284 status: ResponseStatus.SUCCESS,
285 performanceStatistics: [
286 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
287 ...Bootstrap.getInstance().getPerformanceStatistics()!
288 ] as JsonType[]
289 }
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
240fa4da
JB
299 private handleSimulatorState (): ResponsePayload {
300 try {
301 return {
302 status: ResponseStatus.SUCCESS,
303 state: Bootstrap.getInstance().getState()
304 } satisfies ResponsePayload
305 } catch (error) {
306 return {
307 status: ResponseStatus.FAILURE,
308 errorMessage: (error as Error).message,
309 errorStack: (error as Error).stack
310 } satisfies ResponsePayload
311 }
312 }
313
66a7748d 314 private async handleStartSimulator (): Promise<ResponsePayload> {
4ec634b7 315 try {
66a7748d
JB
316 await Bootstrap.getInstance().start()
317 return { status: ResponseStatus.SUCCESS }
a66bbcfe
JB
318 } catch (error) {
319 return {
320 status: ResponseStatus.FAILURE,
321 errorMessage: (error as Error).message,
322 errorStack: (error as Error).stack
323 } satisfies ResponsePayload
4ec634b7 324 }
32de5a57
LM
325 }
326
66a7748d 327 private async handleStopSimulator (): Promise<ResponsePayload> {
4ec634b7 328 try {
66a7748d
JB
329 await Bootstrap.getInstance().stop()
330 return { status: ResponseStatus.SUCCESS }
a66bbcfe
JB
331 } catch (error) {
332 return {
333 status: ResponseStatus.FAILURE,
334 errorMessage: (error as Error).message,
335 errorStack: (error as Error).stack
336 } satisfies ResponsePayload
4ec634b7 337 }
4198ad5c
JB
338 }
339}