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