1 import { BaseError
, type OCPPError
} from
'../../../exception/index.js'
3 BroadcastChannelProcedureName
,
4 type BroadcastChannelRequestPayload
,
5 type ChargingStationInfo
,
6 type ChargingStationOptions
,
12 type ProtocolRequestHandler
,
13 type ProtocolResponse
,
18 type StorageConfiguration
19 } from
'../../../types/index.js'
20 import { Configuration
, isAsyncFunction
, isNotEmptyArray
, logger
} from
'../../../utils/index.js'
21 import { Bootstrap
} from
'../../Bootstrap.js'
22 import { UIServiceWorkerBroadcastChannel
} from
'../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
23 import type { AbstractUIServer
} from
'../AbstractUIServer.js'
25 const moduleName
= 'AbstractUIService'
27 interface AddChargingStationsRequestPayload
extends RequestPayload
{
29 numberOfStations
: number
30 options
?: ChargingStationOptions
33 export abstract class AbstractUIService
{
34 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping
= new Map
<
36 BroadcastChannelProcedureName
38 [ProcedureName
.START_CHARGING_STATION
, BroadcastChannelProcedureName
.START_CHARGING_STATION
],
39 [ProcedureName
.STOP_CHARGING_STATION
, BroadcastChannelProcedureName
.STOP_CHARGING_STATION
],
41 ProcedureName
.DELETE_CHARGING_STATIONS
,
42 BroadcastChannelProcedureName
.DELETE_CHARGING_STATIONS
44 [ProcedureName
.CLOSE_CONNECTION
, BroadcastChannelProcedureName
.CLOSE_CONNECTION
],
45 [ProcedureName
.OPEN_CONNECTION
, BroadcastChannelProcedureName
.OPEN_CONNECTION
],
47 ProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
,
48 BroadcastChannelProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
51 ProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
,
52 BroadcastChannelProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
54 [ProcedureName
.SET_SUPERVISION_URL
, BroadcastChannelProcedureName
.SET_SUPERVISION_URL
],
55 [ProcedureName
.START_TRANSACTION
, BroadcastChannelProcedureName
.START_TRANSACTION
],
56 [ProcedureName
.STOP_TRANSACTION
, BroadcastChannelProcedureName
.STOP_TRANSACTION
],
57 [ProcedureName
.AUTHORIZE
, BroadcastChannelProcedureName
.AUTHORIZE
],
58 [ProcedureName
.BOOT_NOTIFICATION
, BroadcastChannelProcedureName
.BOOT_NOTIFICATION
],
59 [ProcedureName
.STATUS_NOTIFICATION
, BroadcastChannelProcedureName
.STATUS_NOTIFICATION
],
60 [ProcedureName
.HEARTBEAT
, BroadcastChannelProcedureName
.HEARTBEAT
],
61 [ProcedureName
.METER_VALUES
, BroadcastChannelProcedureName
.METER_VALUES
],
62 [ProcedureName
.DATA_TRANSFER
, BroadcastChannelProcedureName
.DATA_TRANSFER
],
64 ProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
,
65 BroadcastChannelProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
68 ProcedureName
.FIRMWARE_STATUS_NOTIFICATION
,
69 BroadcastChannelProcedureName
.FIRMWARE_STATUS_NOTIFICATION
73 protected readonly requestHandlers
: Map
<ProcedureName
, ProtocolRequestHandler
>
74 private readonly version
: ProtocolVersion
75 private readonly uiServer
: AbstractUIServer
76 private readonly uiServiceWorkerBroadcastChannel
: UIServiceWorkerBroadcastChannel
77 private readonly broadcastChannelRequests
: Map
<
78 `${string}-${string}-${string}-${string}-${string}`,
82 constructor (uiServer
: AbstractUIServer
, version
: ProtocolVersion
) {
83 this.uiServer
= uiServer
84 this.version
= version
85 this.requestHandlers
= new Map
<ProcedureName
, ProtocolRequestHandler
>([
86 [ProcedureName
.LIST_TEMPLATES
, this.handleListTemplates
.bind(this)],
87 [ProcedureName
.LIST_CHARGING_STATIONS
, this.handleListChargingStations
.bind(this)],
88 [ProcedureName
.ADD_CHARGING_STATIONS
, this.handleAddChargingStations
.bind(this)],
89 [ProcedureName
.PERFORMANCE_STATISTICS
, this.handlePerformanceStatistics
.bind(this)],
90 [ProcedureName
.SIMULATOR_STATE
, this.handleSimulatorState
.bind(this)],
91 [ProcedureName
.START_SIMULATOR
, this.handleStartSimulator
.bind(this)],
92 [ProcedureName
.STOP_SIMULATOR
, this.handleStopSimulator
.bind(this)]
94 this.uiServiceWorkerBroadcastChannel
= new UIServiceWorkerBroadcastChannel(this)
95 this.broadcastChannelRequests
= new Map
<
96 `${string}-${string}-${string}-${string}-${string}`,
101 public stop (): void {
102 this.broadcastChannelRequests
.clear()
103 this.uiServiceWorkerBroadcastChannel
.close()
106 public async requestHandler (request
: ProtocolRequest
): Promise
<ProtocolResponse
| undefined> {
107 let uuid
: `${string}-${string}-${string}-${string}-${string}` | undefined
108 let command
: ProcedureName
| undefined
109 let requestPayload
: RequestPayload
| undefined
110 let responsePayload
: ResponsePayload
| undefined
112 [uuid
, command
, requestPayload
] = request
114 if (!this.requestHandlers
.has(command
)) {
116 `'${command}' is not implemented to handle message payload ${JSON.stringify(
124 // Call the request handler to build the response payload
125 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
126 const requestHandler
= this.requestHandlers
.get(command
)!
127 if (isAsyncFunction(requestHandler
)) {
128 responsePayload
= await requestHandler(uuid
, command
, requestPayload
)
133 procedureName
?: ProcedureName
,
134 payload
?: RequestPayload
135 ) => undefined | ResponsePayload
136 )(uuid
, command
, requestPayload
)
140 logger
.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error
)
142 hashIds
: requestPayload
?.hashIds
,
143 status: ResponseStatus
.FAILURE
,
147 errorMessage
: (error
as OCPPError
).message
,
148 errorStack
: (error
as OCPPError
).stack
,
149 errorDetails
: (error
as OCPPError
).details
150 } satisfies ResponsePayload
152 if (responsePayload
!= null) {
153 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
154 return this.uiServer
.buildProtocolResponse(uuid
!, responsePayload
)
158 // public sendRequest (
159 // uuid: `${string}-${string}-${string}-${string}-${string}`,
160 // procedureName: ProcedureName,
161 // requestPayload: RequestPayload
163 // this.uiServer.sendRequest(
164 // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
168 public sendResponse (
169 uuid
: `${string}-${string}-${string}-${string}-${string}`,
170 responsePayload
: ResponsePayload
172 if (this.uiServer
.hasResponseHandler(uuid
)) {
173 this.uiServer
.sendResponse(this.uiServer
.buildProtocolResponse(uuid
, responsePayload
))
177 public logPrefix
= (modName
: string, methodName
: string): string => {
178 return this.uiServer
.logPrefix(modName
, methodName
, this.version
)
181 public deleteBroadcastChannelRequest (
182 uuid
: `${string}-${string}-${string}-${string}-${string}`
184 this.broadcastChannelRequests
.delete(uuid
)
187 public getBroadcastChannelExpectedResponses (
188 uuid
: `${string}-${string}-${string}-${string}-${string}`
190 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
191 return this.broadcastChannelRequests
.get(uuid
)!
194 protected handleProtocolRequest (
195 uuid
: `${string}-${string}-${string}-${string}-${string}`,
196 procedureName
: ProcedureName
,
197 payload
: RequestPayload
199 this.sendBroadcastChannelRequest(
201 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
202 AbstractUIService
.ProcedureNameToBroadCastChannelProcedureNameMapping
.get(procedureName
)!,
207 private sendBroadcastChannelRequest (
208 uuid
: `${string}-${string}-${string}-${string}-${string}`,
209 procedureName
: BroadcastChannelProcedureName
,
210 payload
: BroadcastChannelRequestPayload
212 if (isNotEmptyArray(payload
.hashIds
)) {
213 payload
.hashIds
= payload
.hashIds
215 if (this.uiServer
.chargingStations
.has(hashId
)) {
221 'sendBroadcastChannelRequest'
222 )} Charging station with hashId '${hashId}' not found`
226 .filter(hashId
=> hashId
!= null)
228 delete payload
.hashIds
230 const expectedNumberOfResponses
= Array.isArray(payload
.hashIds
)
231 ? payload
.hashIds
.length
232 : this.uiServer
.chargingStations
.size
233 if (expectedNumberOfResponses
=== 0) {
235 'hashIds array in the request payload does not contain any valid charging station hashId'
238 this.uiServiceWorkerBroadcastChannel
.sendRequest([uuid
, procedureName
, payload
])
239 this.broadcastChannelRequests
.set(uuid
, expectedNumberOfResponses
)
242 private handleListTemplates (): ResponsePayload
{
244 status: ResponseStatus
.SUCCESS
,
245 templates
: [...this.uiServer
.chargingStationTemplates
.values()] as JsonType
[]
246 } satisfies ResponsePayload
249 private handleListChargingStations (): ResponsePayload
{
251 status: ResponseStatus
.SUCCESS
,
252 chargingStations
: [...this.uiServer
.chargingStations
.values()] as JsonType
[]
253 } satisfies ResponsePayload
256 private async handleAddChargingStations (
257 _uuid
?: `${string}-${string}-${string}-${string}-${string}`,
258 _procedureName
?: ProcedureName
,
259 requestPayload
?: RequestPayload
260 ): Promise
<ResponsePayload
> {
261 const { template
, numberOfStations
, options
} =
262 requestPayload
as AddChargingStationsRequestPayload
263 if (!Bootstrap
.getInstance().getState().started
) {
265 status: ResponseStatus
.FAILURE
,
267 'Cannot add charging station(s) while the charging stations simulator is not started'
268 } satisfies ResponsePayload
270 if (typeof template
!== 'string' || typeof numberOfStations
!== 'number') {
272 status: ResponseStatus
.FAILURE
,
273 errorMessage
: 'Invalid request payload'
274 } satisfies ResponsePayload
276 if (!this.uiServer
.chargingStationTemplates
.has(template
)) {
278 status: ResponseStatus
.FAILURE
,
279 errorMessage
: `Template '${template}' not found`
280 } satisfies ResponsePayload
282 const succeededStationInfos
: ChargingStationInfo
[] = []
283 const failedStationInfos
: ChargingStationInfo
[] = []
284 let err
: Error | undefined
285 for (let i
= 0; i
< numberOfStations
; i
++) {
286 let stationInfo
: ChargingStationInfo
| undefined
288 stationInfo
= await Bootstrap
.getInstance().addChargingStation(
289 Bootstrap
.getInstance().getLastIndex(template
) + 1,
293 if (stationInfo
!= null) {
294 succeededStationInfos
.push(stationInfo
)
298 if (stationInfo
!= null) {
299 failedStationInfos
.push(stationInfo
)
304 status: err
!= null ? ResponseStatus
.FAILURE
: ResponseStatus
.SUCCESS
,
305 ...(succeededStationInfos
.length
> 0 && {
306 hashIdsSucceeded
: succeededStationInfos
.map(stationInfo
=> stationInfo
.hashId
)
308 ...(failedStationInfos
.length
> 0 && {
309 hashIdsFailed
: failedStationInfos
.map(stationInfo
=> stationInfo
.hashId
)
311 ...(err
!= null && { errorMessage
: err
.message
, errorStack
: err
.stack
})
312 } satisfies ResponsePayload
315 private handlePerformanceStatistics (): ResponsePayload
{
317 Configuration
.getConfigurationSection
<StorageConfiguration
>(
318 ConfigurationSection
.performanceStorage
322 status: ResponseStatus
.FAILURE
,
323 errorMessage
: 'Performance statistics storage is not enabled'
324 } satisfies ResponsePayload
328 status: ResponseStatus
.SUCCESS
,
329 performanceStatistics
: [
330 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
331 ...Bootstrap
.getInstance().getPerformanceStatistics()!
333 } satisfies ResponsePayload
336 status: ResponseStatus
.FAILURE
,
337 errorMessage
: (error
as Error).message
,
338 errorStack
: (error
as Error).stack
339 } satisfies ResponsePayload
343 private handleSimulatorState (): ResponsePayload
{
346 status: ResponseStatus
.SUCCESS
,
347 state
: Bootstrap
.getInstance().getState() as unknown
as JsonObject
348 } satisfies ResponsePayload
351 status: ResponseStatus
.FAILURE
,
352 errorMessage
: (error
as Error).message
,
353 errorStack
: (error
as Error).stack
354 } satisfies ResponsePayload
358 private async handleStartSimulator (): Promise
<ResponsePayload
> {
360 await Bootstrap
.getInstance().start()
361 return { status: ResponseStatus
.SUCCESS
}
364 status: ResponseStatus
.FAILURE
,
365 errorMessage
: (error
as Error).message
,
366 errorStack
: (error
as Error).stack
367 } satisfies ResponsePayload
371 private async handleStopSimulator (): Promise
<ResponsePayload
> {
373 await Bootstrap
.getInstance().stop()
374 return { status: ResponseStatus
.SUCCESS
}
377 status: ResponseStatus
.FAILURE
,
378 errorMessage
: (error
as Error).message
,
379 errorStack
: (error
as Error).stack
380 } satisfies ResponsePayload