1 import { BaseError
, type OCPPError
} from
'../../../exception/index.js'
3 BroadcastChannelProcedureName
,
4 type BroadcastChannelRequestPayload
,
9 type ProtocolRequestHandler
,
10 type ProtocolResponse
,
15 type StorageConfiguration
16 } from
'../../../types/index.js'
17 import { Configuration
, isAsyncFunction
, isNotEmptyArray
, logger
} from
'../../../utils/index.js'
18 import { Bootstrap
} from
'../../Bootstrap.js'
19 import { UIServiceWorkerBroadcastChannel
} from
'../../broadcast-channel/UIServiceWorkerBroadcastChannel.js'
20 import type { AbstractUIServer
} from
'../AbstractUIServer.js'
22 const moduleName
= 'AbstractUIService'
24 export abstract class AbstractUIService
{
25 protected static readonly ProcedureNameToBroadCastChannelProcedureNameMapping
= new Map
<
27 BroadcastChannelProcedureName
29 [ProcedureName
.START_CHARGING_STATION
, BroadcastChannelProcedureName
.START_CHARGING_STATION
],
30 [ProcedureName
.STOP_CHARGING_STATION
, BroadcastChannelProcedureName
.STOP_CHARGING_STATION
],
31 [ProcedureName
.CLOSE_CONNECTION
, BroadcastChannelProcedureName
.CLOSE_CONNECTION
],
32 [ProcedureName
.OPEN_CONNECTION
, BroadcastChannelProcedureName
.OPEN_CONNECTION
],
34 ProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
,
35 BroadcastChannelProcedureName
.START_AUTOMATIC_TRANSACTION_GENERATOR
38 ProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
,
39 BroadcastChannelProcedureName
.STOP_AUTOMATIC_TRANSACTION_GENERATOR
41 [ProcedureName
.SET_SUPERVISION_URL
, BroadcastChannelProcedureName
.SET_SUPERVISION_URL
],
42 [ProcedureName
.START_TRANSACTION
, BroadcastChannelProcedureName
.START_TRANSACTION
],
43 [ProcedureName
.STOP_TRANSACTION
, BroadcastChannelProcedureName
.STOP_TRANSACTION
],
44 [ProcedureName
.AUTHORIZE
, BroadcastChannelProcedureName
.AUTHORIZE
],
45 [ProcedureName
.BOOT_NOTIFICATION
, BroadcastChannelProcedureName
.BOOT_NOTIFICATION
],
46 [ProcedureName
.STATUS_NOTIFICATION
, BroadcastChannelProcedureName
.STATUS_NOTIFICATION
],
47 [ProcedureName
.HEARTBEAT
, BroadcastChannelProcedureName
.HEARTBEAT
],
48 [ProcedureName
.METER_VALUES
, BroadcastChannelProcedureName
.METER_VALUES
],
49 [ProcedureName
.DATA_TRANSFER
, BroadcastChannelProcedureName
.DATA_TRANSFER
],
51 ProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
,
52 BroadcastChannelProcedureName
.DIAGNOSTICS_STATUS_NOTIFICATION
55 ProcedureName
.FIRMWARE_STATUS_NOTIFICATION
,
56 BroadcastChannelProcedureName
.FIRMWARE_STATUS_NOTIFICATION
60 protected readonly requestHandlers
: Map
<ProcedureName
, ProtocolRequestHandler
>
61 private readonly version
: ProtocolVersion
62 private readonly uiServer
: AbstractUIServer
63 private readonly uiServiceWorkerBroadcastChannel
: UIServiceWorkerBroadcastChannel
64 private readonly broadcastChannelRequests
: Map
<string, number>
66 constructor (uiServer
: AbstractUIServer
, version
: ProtocolVersion
) {
67 this.uiServer
= uiServer
68 this.version
= version
69 this.requestHandlers
= new Map
<ProcedureName
, ProtocolRequestHandler
>([
70 [ProcedureName
.LIST_TEMPLATES
, this.handleListTemplates
.bind(this)],
71 [ProcedureName
.LIST_CHARGING_STATIONS
, this.handleListChargingStations
.bind(this)],
72 [ProcedureName
.ADD_CHARGING_STATIONS
, this.handleAddChargingStations
.bind(this)],
73 [ProcedureName
.PERFORMANCE_STATISTICS
, this.handlePerformanceStatistics
.bind(this)],
74 [ProcedureName
.START_SIMULATOR
, this.handleStartSimulator
.bind(this)],
75 [ProcedureName
.STOP_SIMULATOR
, this.handleStopSimulator
.bind(this)]
77 this.uiServiceWorkerBroadcastChannel
= new UIServiceWorkerBroadcastChannel(this)
78 this.broadcastChannelRequests
= new Map
<string, number>()
81 public async requestHandler (request
: ProtocolRequest
): Promise
<ProtocolResponse
| undefined> {
82 let messageId
: string | undefined
83 let command
: ProcedureName
| undefined
84 let requestPayload
: RequestPayload
| undefined
85 let responsePayload
: ResponsePayload
| undefined
87 [messageId
, command
, requestPayload
] = request
89 if (!this.requestHandlers
.has(command
)) {
91 `${command} is not implemented to handle message payload ${JSON.stringify(
99 // Call the request handler to build the response payload
100 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
101 const requestHandler
= this.requestHandlers
.get(command
)!
102 if (isAsyncFunction(requestHandler
)) {
103 responsePayload
= await requestHandler(messageId
, command
, requestPayload
)
108 procedureName
?: ProcedureName
,
109 payload
?: RequestPayload
110 ) => undefined | ResponsePayload
111 )(messageId
, command
, requestPayload
)
115 logger
.error(`${this.logPrefix(moduleName, 'requestHandler')} Handle request error:`, error
)
117 hashIds
: requestPayload
?.hashIds
,
118 status: ResponseStatus
.FAILURE
,
122 errorMessage
: (error
as OCPPError
).message
,
123 errorStack
: (error
as OCPPError
).stack
,
124 errorDetails
: (error
as OCPPError
).details
125 } satisfies ResponsePayload
127 if (responsePayload
!= null) {
128 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
129 return this.uiServer
.buildProtocolResponse(messageId
!, responsePayload
)
133 // public sendRequest (
134 // messageId: string,
135 // procedureName: ProcedureName,
136 // requestPayload: RequestPayload
138 // this.uiServer.sendRequest(
139 // this.uiServer.buildProtocolRequest(messageId, procedureName, requestPayload)
143 public sendResponse (messageId
: string, responsePayload
: ResponsePayload
): void {
144 if (this.uiServer
.hasResponseHandler(messageId
)) {
145 this.uiServer
.sendResponse(this.uiServer
.buildProtocolResponse(messageId
, responsePayload
))
149 public logPrefix
= (modName
: string, methodName
: string): string => {
150 return this.uiServer
.logPrefix(modName
, methodName
, this.version
)
153 public deleteBroadcastChannelRequest (uuid
: string): void {
154 this.broadcastChannelRequests
.delete(uuid
)
157 public getBroadcastChannelExpectedResponses (uuid
: string): number {
158 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
159 return this.broadcastChannelRequests
.get(uuid
)!
162 protected handleProtocolRequest (
164 procedureName
: ProcedureName
,
165 payload
: RequestPayload
167 this.sendBroadcastChannelRequest(
169 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
170 AbstractUIService
.ProcedureNameToBroadCastChannelProcedureNameMapping
.get(procedureName
)!,
175 private sendBroadcastChannelRequest (
177 procedureName
: BroadcastChannelProcedureName
,
178 payload
: BroadcastChannelRequestPayload
180 if (isNotEmptyArray(payload
.hashIds
)) {
181 payload
.hashIds
= payload
.hashIds
183 if (this.uiServer
.chargingStations
.has(hashId
)) {
189 'sendBroadcastChannelRequest'
190 )} Charging station with hashId '${hashId}' not found`
194 .filter(hashId
=> hashId
!= null) as string[]
196 delete payload
.hashIds
198 const expectedNumberOfResponses
= Array.isArray(payload
.hashIds
)
199 ? payload
.hashIds
.length
200 : this.uiServer
.chargingStations
.size
201 if (expectedNumberOfResponses
=== 0) {
203 'hashIds array in the request payload does not contain any valid charging station hashId'
206 this.uiServiceWorkerBroadcastChannel
.sendRequest([uuid
, procedureName
, payload
])
207 this.broadcastChannelRequests
.set(uuid
, expectedNumberOfResponses
)
210 private handleListTemplates (): ResponsePayload
{
212 status: ResponseStatus
.SUCCESS
,
213 templates
: [...this.uiServer
.chargingStationTemplates
.values()] as JsonType
[]
214 } satisfies ResponsePayload
217 private handleListChargingStations (): ResponsePayload
{
219 status: ResponseStatus
.SUCCESS
,
220 chargingStations
: [...this.uiServer
.chargingStations
.values()] as JsonType
[]
221 } satisfies ResponsePayload
224 private async handleAddChargingStations (
226 procedureName
?: ProcedureName
,
227 requestPayload
?: RequestPayload
228 ): Promise
<ResponsePayload
> {
229 const { template
, numberOfStations
} = requestPayload
as {
231 numberOfStations
: number
233 if (!this.uiServer
.chargingStationTemplates
.has(template
)) {
235 status: ResponseStatus
.FAILURE
,
236 errorMessage
: `Template '${template}' not found`
237 } satisfies ResponsePayload
239 for (let i
= 0; i
< numberOfStations
; i
++) {
241 await Bootstrap
.getInstance().addChargingStation(
242 Bootstrap
.getInstance().getLastIndex(template
) + 1,
247 status: ResponseStatus
.FAILURE
,
248 errorMessage
: (error
as Error).message
,
249 errorStack
: (error
as Error).stack
250 } satisfies ResponsePayload
254 status: ResponseStatus
.SUCCESS
258 private handlePerformanceStatistics (): ResponsePayload
{
260 Configuration
.getConfigurationSection
<StorageConfiguration
>(
261 ConfigurationSection
.performanceStorage
265 status: ResponseStatus
.FAILURE
,
266 errorMessage
: 'Performance statistics storage is not enabled'
267 } satisfies ResponsePayload
271 status: ResponseStatus
.SUCCESS
,
272 performanceStatistics
: [
273 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
274 ...Bootstrap
.getInstance().getPerformanceStatistics()!
279 status: ResponseStatus
.FAILURE
,
280 errorMessage
: (error
as Error).message
,
281 errorStack
: (error
as Error).stack
282 } satisfies ResponsePayload
286 private async handleStartSimulator (): Promise
<ResponsePayload
> {
288 await Bootstrap
.getInstance().start()
289 return { status: ResponseStatus
.SUCCESS
}
292 status: ResponseStatus
.FAILURE
,
293 errorMessage
: (error
as Error).message
,
294 errorStack
: (error
as Error).stack
295 } satisfies ResponsePayload
299 private async handleStopSimulator (): Promise
<ResponsePayload
> {
301 await Bootstrap
.getInstance().stop()
302 return { status: ResponseStatus
.SUCCESS
}
305 status: ResponseStatus
.FAILURE
,
306 errorMessage
: (error
as Error).message
,
307 errorStack
: (error
as Error).stack
308 } satisfies ResponsePayload