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