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