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