87f693f8342e7e055232735a7afa00ff155ace8e
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
1 import { secondsToMilliseconds } from 'date-fns'
2
3 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel.js'
4 import { BaseError, type OCPPError } from '../../exception/index.js'
5 import {
6 AuthorizationStatus,
7 type AuthorizeRequest,
8 type AuthorizeResponse,
9 type BootNotificationRequest,
10 type BootNotificationResponse,
11 BroadcastChannelProcedureName,
12 type BroadcastChannelRequest,
13 type BroadcastChannelRequestPayload,
14 type BroadcastChannelResponsePayload,
15 type DataTransferRequest,
16 type DataTransferResponse,
17 DataTransferStatus,
18 type DiagnosticsStatusNotificationRequest,
19 type DiagnosticsStatusNotificationResponse,
20 type EmptyObject,
21 type FirmwareStatusNotificationRequest,
22 type FirmwareStatusNotificationResponse,
23 type HeartbeatRequest,
24 type HeartbeatResponse,
25 type MessageEvent,
26 type MeterValuesRequest,
27 type MeterValuesResponse,
28 RegistrationStatusEnumType,
29 RequestCommand,
30 type RequestParams,
31 ResponseStatus,
32 StandardParametersKey,
33 type StartTransactionRequest,
34 type StartTransactionResponse,
35 type StatusNotificationRequest,
36 type StatusNotificationResponse,
37 type StopTransactionRequest,
38 type StopTransactionResponse
39 } from '../../types/index.js'
40 import {
41 Constants,
42 convertToInt,
43 isEmptyObject,
44 isNullOrUndefined,
45 logger
46 } from '../../utils/index.js'
47 import type { ChargingStation } from '../ChargingStation.js'
48 import { getConfigurationKey } from '../ConfigurationKeyUtils.js'
49 import { buildMeterValue } from '../ocpp/index.js'
50
51 const moduleName = 'ChargingStationWorkerBroadcastChannel'
52
53 type CommandResponse =
54 | EmptyObject
55 | StartTransactionResponse
56 | StopTransactionResponse
57 | AuthorizeResponse
58 | BootNotificationResponse
59 | HeartbeatResponse
60 | DataTransferResponse
61
62 type CommandHandler = (
63 requestPayload?: BroadcastChannelRequestPayload
64 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
65 ) => Promise<CommandResponse | void> | void
66
67 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
68 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>
69 private readonly chargingStation: ChargingStation
70
71 constructor (chargingStation: ChargingStation) {
72 super()
73 const requestParams: RequestParams = {
74 throwError: true
75 }
76 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
77 [
78 BroadcastChannelProcedureName.START_CHARGING_STATION,
79 () => {
80 this.chargingStation.start()
81 }
82 ],
83 [
84 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
85 async () => {
86 await this.chargingStation.stop()
87 }
88 ],
89 [
90 BroadcastChannelProcedureName.OPEN_CONNECTION,
91 () => {
92 this.chargingStation.openWSConnection()
93 }
94 ],
95 [
96 BroadcastChannelProcedureName.CLOSE_CONNECTION,
97 () => {
98 this.chargingStation.closeWSConnection()
99 }
100 ],
101 [
102 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
103 (requestPayload?: BroadcastChannelRequestPayload) => {
104 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds)
105 }
106 ],
107 [
108 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
109 (requestPayload?: BroadcastChannelRequestPayload) => {
110 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds)
111 }
112 ],
113 [
114 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
115 (requestPayload?: BroadcastChannelRequestPayload) => {
116 this.chargingStation.setSupervisionUrl(requestPayload?.url as string)
117 }
118 ],
119 [
120 BroadcastChannelProcedureName.START_TRANSACTION,
121 async (requestPayload?: BroadcastChannelRequestPayload) =>
122 await this.chargingStation.ocppRequestService.requestHandler<
123 StartTransactionRequest,
124 StartTransactionResponse
125 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams)
126 ],
127 [
128 BroadcastChannelProcedureName.STOP_TRANSACTION,
129 async (requestPayload?: BroadcastChannelRequestPayload) =>
130 await this.chargingStation.ocppRequestService.requestHandler<
131 StopTransactionRequest,
132 StartTransactionResponse
133 >(
134 this.chargingStation,
135 RequestCommand.STOP_TRANSACTION,
136 {
137 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
138 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
139 requestPayload!.transactionId!,
140 true
141 ),
142 ...requestPayload
143 },
144 requestParams
145 )
146 ],
147 [
148 BroadcastChannelProcedureName.AUTHORIZE,
149 async (requestPayload?: BroadcastChannelRequestPayload) =>
150 await this.chargingStation.ocppRequestService.requestHandler<
151 AuthorizeRequest,
152 AuthorizeResponse
153 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams)
154 ],
155 [
156 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
157 async (requestPayload?: BroadcastChannelRequestPayload) => {
158 this.chargingStation.bootNotificationResponse =
159 await this.chargingStation.ocppRequestService.requestHandler<
160 BootNotificationRequest,
161 BootNotificationResponse
162 >(
163 this.chargingStation,
164 RequestCommand.BOOT_NOTIFICATION,
165 {
166 ...this.chargingStation.bootNotificationRequest,
167 ...requestPayload
168 },
169 {
170 skipBufferingOnError: true,
171 throwError: true
172 }
173 )
174 return this.chargingStation.bootNotificationResponse
175 }
176 ],
177 [
178 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
179 async (requestPayload?: BroadcastChannelRequestPayload) =>
180 await this.chargingStation.ocppRequestService.requestHandler<
181 StatusNotificationRequest,
182 StatusNotificationResponse
183 >(this.chargingStation, RequestCommand.STATUS_NOTIFICATION, requestPayload, requestParams)
184 ],
185 [
186 BroadcastChannelProcedureName.HEARTBEAT,
187 async (requestPayload?: BroadcastChannelRequestPayload) =>
188 await this.chargingStation.ocppRequestService.requestHandler<
189 HeartbeatRequest,
190 HeartbeatResponse
191 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams)
192 ],
193 [
194 BroadcastChannelProcedureName.METER_VALUES,
195 async (requestPayload?: BroadcastChannelRequestPayload) => {
196 const configuredMeterValueSampleInterval = getConfigurationKey(
197 chargingStation,
198 StandardParametersKey.MeterValueSampleInterval
199 )
200 return await this.chargingStation.ocppRequestService.requestHandler<
201 MeterValuesRequest,
202 MeterValuesResponse
203 >(
204 this.chargingStation,
205 RequestCommand.METER_VALUES,
206 {
207 meterValue: [
208 buildMeterValue(
209 this.chargingStation,
210 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
211 requestPayload!.connectorId!,
212 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
213 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
214 .transactionId!,
215 configuredMeterValueSampleInterval !== undefined
216 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
217 : Constants.DEFAULT_METER_VALUES_INTERVAL
218 )
219 ],
220 ...requestPayload
221 },
222 requestParams
223 )
224 }
225 ],
226 [
227 BroadcastChannelProcedureName.DATA_TRANSFER,
228 async (requestPayload?: BroadcastChannelRequestPayload) =>
229 await this.chargingStation.ocppRequestService.requestHandler<
230 DataTransferRequest,
231 DataTransferResponse
232 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams)
233 ],
234 [
235 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
236 async (requestPayload?: BroadcastChannelRequestPayload) =>
237 await this.chargingStation.ocppRequestService.requestHandler<
238 DiagnosticsStatusNotificationRequest,
239 DiagnosticsStatusNotificationResponse
240 >(
241 this.chargingStation,
242 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
243 requestPayload,
244 requestParams
245 )
246 ],
247 [
248 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
249 async (requestPayload?: BroadcastChannelRequestPayload) =>
250 await this.chargingStation.ocppRequestService.requestHandler<
251 FirmwareStatusNotificationRequest,
252 FirmwareStatusNotificationResponse
253 >(
254 this.chargingStation,
255 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
256 requestPayload,
257 requestParams
258 )
259 ]
260 ])
261 this.chargingStation = chargingStation
262 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void
263 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void
264 }
265
266 private async requestHandler (messageEvent: MessageEvent): Promise<void> {
267 const validatedMessageEvent = this.validateMessageEvent(messageEvent)
268 if (validatedMessageEvent === false) {
269 return
270 }
271 if (this.isResponse(validatedMessageEvent.data)) {
272 return
273 }
274 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest
275 if (
276 !isNullOrUndefined(requestPayload.hashIds) &&
277 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
278 ) {
279 return
280 }
281 if (!isNullOrUndefined(requestPayload.hashId)) {
282 logger.error(
283 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
284 )
285 return
286 }
287 let responsePayload: BroadcastChannelResponsePayload | undefined
288 let commandResponse: CommandResponse | undefined
289 try {
290 commandResponse = await this.commandHandler(command, requestPayload)
291 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse)) {
292 responsePayload = {
293 hashId: this.chargingStation.stationInfo.hashId,
294 status: ResponseStatus.SUCCESS
295 }
296 } else {
297 responsePayload = this.commandResponseToResponsePayload(
298 command,
299 requestPayload,
300 commandResponse
301 )
302 }
303 } catch (error) {
304 logger.error(
305 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
306 error
307 )
308 responsePayload = {
309 hashId: this.chargingStation.stationInfo.hashId,
310 status: ResponseStatus.FAILURE,
311 command,
312 requestPayload,
313 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
314 commandResponse: commandResponse!,
315 errorMessage: (error as OCPPError).message,
316 errorStack: (error as OCPPError).stack,
317 errorDetails: (error as OCPPError).details
318 }
319 } finally {
320 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
321 this.sendResponse([uuid, responsePayload!])
322 }
323 }
324
325 private messageErrorHandler (messageEvent: MessageEvent): void {
326 logger.error(
327 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
328 messageEvent
329 )
330 }
331
332 private async commandHandler (
333 command: BroadcastChannelProcedureName,
334 requestPayload: BroadcastChannelRequestPayload
335 // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
336 ): Promise<void | CommandResponse> {
337 if (this.commandHandlers.has(command)) {
338 this.cleanRequestPayload(command, requestPayload)
339 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
340 return await this.commandHandlers.get(command)!(requestPayload)
341 }
342 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`)
343 }
344
345 private cleanRequestPayload (
346 command: BroadcastChannelProcedureName,
347 requestPayload: BroadcastChannelRequestPayload
348 ): void {
349 delete requestPayload.hashId
350 delete requestPayload.hashIds
351 ![
352 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
353 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR
354 ].includes(command) && delete requestPayload.connectorIds
355 }
356
357 private commandResponseToResponsePayload (
358 command: BroadcastChannelProcedureName,
359 requestPayload: BroadcastChannelRequestPayload,
360 commandResponse: CommandResponse
361 ): BroadcastChannelResponsePayload {
362 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse)
363 if (responseStatus === ResponseStatus.SUCCESS) {
364 return {
365 hashId: this.chargingStation.stationInfo.hashId,
366 status: responseStatus
367 }
368 }
369 return {
370 hashId: this.chargingStation.stationInfo.hashId,
371 status: responseStatus,
372 command,
373 requestPayload,
374 commandResponse
375 }
376 }
377
378 private commandResponseToResponseStatus (
379 command: BroadcastChannelProcedureName,
380 commandResponse: CommandResponse
381 ): ResponseStatus {
382 switch (command) {
383 case BroadcastChannelProcedureName.START_TRANSACTION:
384 case BroadcastChannelProcedureName.STOP_TRANSACTION:
385 case BroadcastChannelProcedureName.AUTHORIZE:
386 if (
387 (
388 commandResponse as
389 | StartTransactionResponse
390 | StopTransactionResponse
391 | AuthorizeResponse
392 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
393 ) {
394 return ResponseStatus.SUCCESS
395 }
396 return ResponseStatus.FAILURE
397 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
398 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
399 return ResponseStatus.SUCCESS
400 }
401 return ResponseStatus.FAILURE
402 case BroadcastChannelProcedureName.DATA_TRANSFER:
403 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
404 return ResponseStatus.SUCCESS
405 }
406 return ResponseStatus.FAILURE
407 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
408 case BroadcastChannelProcedureName.METER_VALUES:
409 if (isEmptyObject(commandResponse)) {
410 return ResponseStatus.SUCCESS
411 }
412 return ResponseStatus.FAILURE
413 case BroadcastChannelProcedureName.HEARTBEAT:
414 if ('currentTime' in commandResponse) {
415 return ResponseStatus.SUCCESS
416 }
417 return ResponseStatus.FAILURE
418 default:
419 return ResponseStatus.FAILURE
420 }
421 }
422 }