Merge branch 'main' of github.com:SAP/e-mobility-charging-stations-simulator
[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 !Utils.isNullOrUndefined(requestPayload?.hashIds) &&
262 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
263 ) {
264 return;
265 }
266 if (!Utils.isNullOrUndefined(requestPayload?.hashId)) {
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 Utils.isNullOrUndefined(commandResponse) ||
278 Utils.isEmptyObject(commandResponse as CommandResponse)
279 ) {
280 responsePayload = {
281 hashId: this.chargingStation.stationInfo.hashId,
282 status: ResponseStatus.SUCCESS,
283 };
284 } else {
285 responsePayload = this.commandResponseToResponsePayload(
286 command,
287 requestPayload,
288 commandResponse as CommandResponse
289 );
290 }
291 } catch (error) {
292 logger.error(
293 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
294 error
295 );
296 responsePayload = {
297 hashId: this.chargingStation.stationInfo.hashId,
298 status: ResponseStatus.FAILURE,
299 command,
300 requestPayload,
301 commandResponse: commandResponse as CommandResponse,
302 errorMessage: (error as Error).message,
303 errorStack: (error as Error).stack,
304 errorDetails: (error as OCPPError).details,
305 };
306 } finally {
307 this.sendResponse([uuid, responsePayload]);
308 }
309 }
310
311 private messageErrorHandler(messageEvent: MessageEvent): void {
312 logger.error(
313 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
314 messageEvent
315 );
316 }
317
318 private async commandHandler(
319 command: BroadcastChannelProcedureName,
320 requestPayload: BroadcastChannelRequestPayload
321 ): Promise<CommandResponse | void> {
322 if (this.commandHandlers.has(command) === true) {
323 this.cleanRequestPayload(command, requestPayload);
324 return this.commandHandlers.get(command)(requestPayload);
325 }
326 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
327 }
328
329 private cleanRequestPayload(
330 command: BroadcastChannelProcedureName,
331 requestPayload: BroadcastChannelRequestPayload
332 ): void {
333 delete requestPayload.hashId;
334 delete requestPayload.hashIds;
335 [
336 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
337 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
338 ].includes(command) === false && delete requestPayload.connectorIds;
339 }
340
341 private commandResponseToResponsePayload(
342 command: BroadcastChannelProcedureName,
343 requestPayload: BroadcastChannelRequestPayload,
344 commandResponse: CommandResponse
345 ): BroadcastChannelResponsePayload {
346 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
347 if (responseStatus === ResponseStatus.SUCCESS) {
348 return {
349 hashId: this.chargingStation.stationInfo.hashId,
350 status: responseStatus,
351 };
352 }
353 return {
354 hashId: this.chargingStation.stationInfo.hashId,
355 status: responseStatus,
356 command,
357 requestPayload,
358 commandResponse,
359 };
360 }
361
362 private commandResponseToResponseStatus(
363 command: BroadcastChannelProcedureName,
364 commandResponse: CommandResponse
365 ): ResponseStatus {
366 switch (command) {
367 case BroadcastChannelProcedureName.START_TRANSACTION:
368 case BroadcastChannelProcedureName.STOP_TRANSACTION:
369 case BroadcastChannelProcedureName.AUTHORIZE:
370 if (
371 (
372 commandResponse as
373 | StartTransactionResponse
374 | StopTransactionResponse
375 | AuthorizeResponse
376 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
377 ) {
378 return ResponseStatus.SUCCESS;
379 }
380 return ResponseStatus.FAILURE;
381 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
382 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
383 return ResponseStatus.SUCCESS;
384 }
385 return ResponseStatus.FAILURE;
386 case BroadcastChannelProcedureName.DATA_TRANSFER:
387 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
388 return ResponseStatus.SUCCESS;
389 }
390 return ResponseStatus.FAILURE;
391 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
392 case BroadcastChannelProcedureName.METER_VALUES:
393 if (Utils.isEmptyObject(commandResponse) === true) {
394 return ResponseStatus.SUCCESS;
395 }
396 return ResponseStatus.FAILURE;
397 case BroadcastChannelProcedureName.HEARTBEAT:
398 if ('currentTime' in commandResponse) {
399 return ResponseStatus.SUCCESS;
400 }
401 return ResponseStatus.FAILURE;
402 default:
403 return ResponseStatus.FAILURE;
404 }
405 }
406 }