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 export abstract class AbstractUIService
{
28 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping
= new Map
<
30 BroadcastChannelProcedureName
32 [ProcedureName
.START_CHARGING_STATION
, BroadcastChannelProcedureName
.START_CHARGING_STATION
],
33 [ProcedureName
.STOP_CHARGING_STATION
, BroadcastChannelProcedureName
.STOP_CHARGING_STATION
],
35 ProcedureName
.DELETE_CHARGING_STATIONS
,
36 BroadcastChannelProcedureName
.DELETE_CHARGING_STATIONS
38 [ProcedureName
.CLOSE_CONNECTION
, BroadcastChannelProcedureName
.CLOSE_CONNECTION
],
39 [ProcedureName
.OPEN_CONNECTION
, BroadcastChannelProcedureName
.OPEN_CONNECTION
],
41 ProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
,
42 BroadcastChannelProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
45 ProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
,
46 BroadcastChannelProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
48 [ProcedureName
.SET_SUPERVISION_URL
, BroadcastChannelProcedureName
.SET_SUPERVISION_URL
],
49 [ProcedureName
.START_TRANSACTION
, BroadcastChannelProcedureName
.START_TRANSACTION
],
50 [ProcedureName
.STOP_TRANSACTION
, BroadcastChannelProcedureName
.STOP_TRANSACTION
],
51 [ProcedureName
.AUTHORIZE
, BroadcastChannelProcedureName
.AUTHORIZE
],
52 [ProcedureName
.BOOT_NOTIFICATION
, BroadcastChannelProcedureName
.BOOT_NOTIFICATION
],
53 [ProcedureName
.STATUS_NOTIFICATION
, BroadcastChannelProcedureName
.STATUS_NOTIFICATION
],
54 [ProcedureName
.HEARTBEAT
, BroadcastChannelProcedureName
.HEARTBEAT
],
55 [ProcedureName
.METER_VALUES
, BroadcastChannelProcedureName
.METER_VALUES
],
56 [ProcedureName
.DATA_TRANSFER
, BroadcastChannelProcedureName
.DATA_TRANSFER
],
58 ProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
,
59 BroadcastChannelProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
62 ProcedureName
.FIRMWARE_STATUS_NOTIFICATION
,
63 BroadcastChannelProcedureName
.FIRMWARE_STATUS_NOTIFICATION
67 protected readonly requestHandlers
: Map
<ProcedureName
, ProtocolRequestHandler
>
68 private readonly version
: ProtocolVersion
69 private readonly uiServer
: AbstractUIServer
70 private readonly uiServiceWorkerBroadcastChannel
: UIServiceWorkerBroadcastChannel
71 private readonly broadcastChannelRequests
: Map
<
72 `${string}-${string}-${string}-${string}-${string}`,
76 constructor (uiServer
: AbstractUIServer
, version
: ProtocolVersion
) {
77 this.uiServer
= uiServer
78 this.version
= version
79 this.requestHandlers
= new Map
<ProcedureName
, ProtocolRequestHandler
>([
80 [ProcedureName
.LIST_TEMPLATES
, this.handleListTemplates
.bind(this)],
81 [ProcedureName
.LIST_CHARGING_STATIONS
, this.handleListChargingStations
.bind(this)],
82 [ProcedureName
.ADD_CHARGING_STATIONS
, this.handleAddChargingStations
.bind(this)],
83 [ProcedureName
.PERFORMANCE_STATISTICS
, this.handlePerformanceStatistics
.bind(this)],
84 [ProcedureName
.SIMULATOR_STATE
, this.handleSimulatorState
.bind(this)],
85 [ProcedureName
.START_SIMULATOR
, this.handleStartSimulator
.bind(this)],
86 [ProcedureName
.STOP_SIMULATOR
, this.handleStopSimulator
.bind(this)]
88 this.uiServiceWorkerBroadcastChannel
= new UIServiceWorkerBroadcastChannel(this)
89 this.broadcastChannelRequests
= new Map
<
90 `${string}-${string}-${string}-${string}-${string}`,
95 public stop (): void {
96 this.broadcastChannelRequests
.clear()
97 this.uiServiceWorkerBroadcastChannel
.close()
100 public async requestHandler (request
: ProtocolRequest
): Promise
<ProtocolResponse
| undefined> {
101 let uuid
: `${string}-${string}-${string}-${string}-${string}` | undefined
102 let command
: ProcedureName
| undefined
103 let requestPayload
: RequestPayload
| undefined
104 let responsePayload
: ResponsePayload
| undefined
106 [uuid
, command
, requestPayload
] = request
108 if (!this.requestHandlers
.has(command
)) {
110 `'${command}' is not implemented to handle message payload ${JSON.stringify(
118 // Call the request handler to build the response payload
119 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
120 const requestHandler
= this.requestHandlers
.get(command
)!
121 if (isAsyncFunction(requestHandler
)) {
122 responsePayload
= await requestHandler(uuid
, command
, requestPayload
)
127 procedureName
?: ProcedureName
,
128 payload
?: RequestPayload
129 ) => undefined | ResponsePayload
130 )(uuid
, command
, requestPayload
)
134 logger
.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error
)
136 hashIds
: requestPayload
?.hashIds
,
137 status: ResponseStatus
.FAILURE
,
141 errorMessage
: (error
as OCPPError
).message
,
142 errorStack
: (error
as OCPPError
).stack
,
143 errorDetails
: (error
as OCPPError
).details
144 } satisfies ResponsePayload
146 if (responsePayload
!= null) {
147 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
148 return this.uiServer
.buildProtocolResponse(uuid
!, responsePayload
)
152 // public sendRequest (
153 // uuid: `${string}-${string}-${string}-${string}-${string}`,
154 // procedureName: ProcedureName,
155 // requestPayload: RequestPayload
157 // this.uiServer.sendRequest(
158 // this.uiServer.buildProtocolRequest(uuid, procedureName, requestPayload)
162 public sendResponse (
163 uuid
: `${string}-${string}-${string}-${string}-${string}`,
164 responsePayload
: ResponsePayload
166 if (this.uiServer
.hasResponseHandler(uuid
)) {
167 this.uiServer
.sendResponse(this.uiServer
.buildProtocolResponse(uuid
, responsePayload
))
171 public logPrefix
= (modName
: string, methodName
: string): string => {
172 return this.uiServer
.logPrefix(modName
, methodName
, this.version
)
175 public deleteBroadcastChannelRequest (
176 uuid
: `${string}-${string}-${string}-${string}-${string}`
178 this.broadcastChannelRequests
.delete(uuid
)
181 public getBroadcastChannelExpectedResponses (
182 uuid
: `${string}-${string}-${string}-${string}-${string}`
184 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
185 return this.broadcastChannelRequests
.get(uuid
)!
188 protected handleProtocolRequest (
189 uuid
: `${string}-${string}-${string}-${string}-${string}`,
190 procedureName
: ProcedureName
,
191 payload
: RequestPayload
193 this.sendBroadcastChannelRequest(
195 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
196 AbstractUIService
.ProcedureNameToBroadCastChannelProcedureNameMapping
.get(procedureName
)!,
201 private sendBroadcastChannelRequest (
202 uuid
: `${string}-${string}-${string}-${string}-${string}`,
203 procedureName
: BroadcastChannelProcedureName
,
204 payload
: BroadcastChannelRequestPayload
206 if (isNotEmptyArray(payload
.hashIds
)) {
207 payload
.hashIds
= payload
.hashIds
209 if (this.uiServer
.chargingStations
.has(hashId
)) {
215 'sendBroadcastChannelRequest'
216 )} Charging station with hashId '${hashId}' not found`
220 .filter(hashId
=> hashId
!= null) as string[]
222 delete payload
.hashIds
224 const expectedNumberOfResponses
= Array.isArray(payload
.hashIds
)
225 ? payload
.hashIds
.length
226 : this.uiServer
.chargingStations
.size
227 if (expectedNumberOfResponses
=== 0) {
229 'hashIds array in the request payload does not contain any valid charging station hashId'
232 this.uiServiceWorkerBroadcastChannel
.sendRequest([uuid
, procedureName
, payload
])
233 this.broadcastChannelRequests
.set(uuid
, expectedNumberOfResponses
)
236 private handleListTemplates (): ResponsePayload
{
238 status: ResponseStatus
.SUCCESS
,
239 templates
: [...this.uiServer
.chargingStationTemplates
.values()] as JsonType
[]
240 } satisfies ResponsePayload
243 private handleListChargingStations (): ResponsePayload
{
245 status: ResponseStatus
.SUCCESS
,
246 chargingStations
: [...this.uiServer
.chargingStations
.values()] as JsonType
[]
247 } satisfies ResponsePayload
250 private async handleAddChargingStations (
251 _uuid
?: `${string}-${string}-${string}-${string}-${string}`,
252 _procedureName
?: ProcedureName
,
253 requestPayload
?: RequestPayload
254 ): Promise
<ResponsePayload
> {
255 const { template
, numberOfStations
, options
} = requestPayload
as {
257 numberOfStations
: number
258 options
?: ChargingStationOptions
260 if (!Bootstrap
.getInstance().getState().started
) {
262 status: ResponseStatus
.FAILURE
,
264 'Cannot add charging station(s) while the charging stations simulator is not started'
265 } satisfies ResponsePayload
267 if (typeof template
!== 'string' || typeof numberOfStations
!== 'number') {
269 status: ResponseStatus
.FAILURE
,
270 errorMessage
: 'Invalid request payload'
271 } satisfies ResponsePayload
273 if (!this.uiServer
.chargingStationTemplates
.has(template
)) {
275 status: ResponseStatus
.FAILURE
,
276 errorMessage
: `Template '${template}' not found`
277 } satisfies ResponsePayload
279 const stationInfos
: ChargingStationInfo
[] = []
280 for (let i
= 0; i
< numberOfStations
; i
++) {
281 let stationInfo
: ChargingStationInfo
| undefined
283 stationInfo
= await Bootstrap
.getInstance().addChargingStation(
284 Bootstrap
.getInstance().getLastIndex(template
) + 1,
288 if (stationInfo
!= null) {
289 stationInfos
.push(stationInfo
)
293 status: ResponseStatus
.FAILURE
,
294 ...(stationInfo
?.hashId
!= null && { hashIdsFailed
: [stationInfo
.hashId
] }),
295 errorMessage
: (error
as Error).message
,
296 errorStack
: (error
as Error).stack
297 } satisfies ResponsePayload
301 status: ResponseStatus
.SUCCESS
,
302 hashIdsSucceeded
: stationInfos
.map(stationInfo
=> stationInfo
.hashId
)
306 private handlePerformanceStatistics (): ResponsePayload
{
308 Configuration
.getConfigurationSection
<StorageConfiguration
>(
309 ConfigurationSection
.performanceStorage
313 status: ResponseStatus
.FAILURE
,
314 errorMessage
: 'Performance statistics storage is not enabled'
315 } satisfies ResponsePayload
319 status: ResponseStatus
.SUCCESS
,
320 performanceStatistics
: [
321 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
322 ...Bootstrap
.getInstance().getPerformanceStatistics()!
327 status: ResponseStatus
.FAILURE
,
328 errorMessage
: (error
as Error).message
,
329 errorStack
: (error
as Error).stack
330 } satisfies ResponsePayload
334 private handleSimulatorState (): ResponsePayload
{
337 status: ResponseStatus
.SUCCESS
,
338 state
: Bootstrap
.getInstance().getState() as unknown
as JsonObject
339 } satisfies ResponsePayload
342 status: ResponseStatus
.FAILURE
,
343 errorMessage
: (error
as Error).message
,
344 errorStack
: (error
as Error).stack
345 } satisfies ResponsePayload
349 private async handleStartSimulator (): Promise
<ResponsePayload
> {
351 await Bootstrap
.getInstance().start()
352 return { status: ResponseStatus
.SUCCESS
}
355 status: ResponseStatus
.FAILURE
,
356 errorMessage
: (error
as Error).message
,
357 errorStack
: (error
as Error).stack
358 } satisfies ResponsePayload
362 private async handleStopSimulator (): Promise
<ResponsePayload
> {
364 await Bootstrap
.getInstance().stop()
365 return { status: ResponseStatus
.SUCCESS
}
368 status: ResponseStatus
.FAILURE
,
369 errorMessage
: (error
as Error).message
,
370 errorStack
: (error
as Error).stack
371 } satisfies ResponsePayload