refactor(simulator): use helper for undefined or null checks
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
2896e06d
JB
1import {
2 type ChargingStation,
3 ChargingStationConfigurationUtils,
4 WorkerBroadcastChannel,
5} from './internal';
368a6eda 6import { OCPP16ServiceUtils } from './ocpp';
268a74bb 7import { BaseError, type OCPPError } from '../exception';
10db00b2 8import {
268a74bb
JB
9 AuthorizationStatus,
10 type AuthorizeRequest,
11 type AuthorizeResponse,
8bfbc743 12 type BootNotificationRequest,
8bfbc743 13 type BootNotificationResponse,
268a74bb
JB
14 BroadcastChannelProcedureName,
15 type BroadcastChannelRequest,
16 type BroadcastChannelRequestPayload,
17 type BroadcastChannelResponsePayload,
18 type DataTransferRequest,
91a7d3ea
JB
19 type DataTransferResponse,
20 DataTransferStatus,
268a74bb 21 type DiagnosticsStatusNotificationRequest,
c9a4f9ea 22 type DiagnosticsStatusNotificationResponse,
268a74bb 23 type FirmwareStatusNotificationRequest,
c9a4f9ea 24 type FirmwareStatusNotificationResponse,
268a74bb 25 type HeartbeatRequest,
8bfbc743 26 type HeartbeatResponse,
268a74bb
JB
27 type MessageEvent,
28 type MeterValuesRequest,
8bfbc743 29 type MeterValuesResponse,
d270cc87 30 RegistrationStatusEnumType,
268a74bb
JB
31 RequestCommand,
32 type RequestParams,
33 ResponseStatus,
34 StandardParametersKey,
8bfbc743
JB
35 type StartTransactionRequest,
36 type StartTransactionResponse,
268a74bb
JB
37 type StatusNotificationRequest,
38 type StatusNotificationResponse,
8bfbc743
JB
39 type StopTransactionRequest,
40 type StopTransactionResponse,
268a74bb 41} from '../types';
60a74391 42import { Constants, Utils, logger } from '../utils';
89b7a234 43
4e3ff94d
JB
44const moduleName = 'ChargingStationWorkerBroadcastChannel';
45
a9ed42b2
JB
46type CommandResponse =
47 | StartTransactionResponse
48 | StopTransactionResponse
1984f194 49 | AuthorizeResponse
8bfbc743 50 | BootNotificationResponse
10db00b2 51 | StatusNotificationResponse
d3195f0a 52 | HeartbeatResponse
91a7d3ea 53 | MeterValuesResponse
c9a4f9ea
JB
54 | DataTransferResponse
55 | DiagnosticsStatusNotificationResponse
56 | FirmwareStatusNotificationResponse;
89b7a234 57
d273692c
JB
58type CommandHandler = (
59 requestPayload?: BroadcastChannelRequestPayload
60) => Promise<CommandResponse | void> | void;
61
268a74bb 62export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
d273692c 63 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
89b7a234
JB
64 private readonly chargingStation: ChargingStation;
65
66 constructor(chargingStation: ChargingStation) {
1598b27c 67 super();
8ec8e3d0
JB
68 const requestParams: RequestParams = {
69 throwError: true,
70 };
d273692c 71 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
9d73266c
JB
72 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
73 [
74 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
d273692c 75 async () => this.chargingStation.stop(),
9d73266c
JB
76 ],
77 [
78 BroadcastChannelProcedureName.OPEN_CONNECTION,
79 () => this.chargingStation.openWSConnection(),
80 ],
81 [
82 BroadcastChannelProcedureName.CLOSE_CONNECTION,
83 () => this.chargingStation.closeWSConnection(),
84 ],
623b39b5
JB
85 [
86 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
87 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 88 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5
JB
89 ],
90 [
91 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
92 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 93 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5 94 ],
269de583
JB
95 [
96 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
97 (requestPayload?: BroadcastChannelRequestPayload) =>
98 this.chargingStation.setSupervisionUrl(requestPayload?.url as string),
99 ],
9d73266c
JB
100 [
101 BroadcastChannelProcedureName.START_TRANSACTION,
102 async (requestPayload?: BroadcastChannelRequestPayload) =>
103 this.chargingStation.ocppRequestService.requestHandler<
104 StartTransactionRequest,
105 StartTransactionResponse
8ec8e3d0 106 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
9d73266c
JB
107 ],
108 [
109 BroadcastChannelProcedureName.STOP_TRANSACTION,
110 async (requestPayload?: BroadcastChannelRequestPayload) =>
111 this.chargingStation.ocppRequestService.requestHandler<
112 StopTransactionRequest,
113 StartTransactionResponse
1969f643
JB
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 ),
9d73266c 126 ],
1984f194
JB
127 [
128 BroadcastChannelProcedureName.AUTHORIZE,
129 async (requestPayload?: BroadcastChannelRequestPayload) =>
130 this.chargingStation.ocppRequestService.requestHandler<
131 AuthorizeRequest,
132 AuthorizeResponse
8ec8e3d0 133 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
1984f194 134 ],
8bfbc743
JB
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,
8ec8e3d0 151 throwError: true,
8bfbc743
JB
152 }
153 );
154 return this.chargingStation.bootNotificationResponse;
155 },
156 ],
9d73266c
JB
157 [
158 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
159 async (requestPayload?: BroadcastChannelRequestPayload) =>
160 this.chargingStation.ocppRequestService.requestHandler<
161 StatusNotificationRequest,
162 StatusNotificationResponse
8ec8e3d0
JB
163 >(
164 this.chargingStation,
165 RequestCommand.STATUS_NOTIFICATION,
166 requestPayload,
167 requestParams
168 ),
9d73266c
JB
169 ],
170 [
171 BroadcastChannelProcedureName.HEARTBEAT,
1984f194
JB
172 async (requestPayload?: BroadcastChannelRequestPayload) =>
173 this.chargingStation.ocppRequestService.requestHandler<
9d73266c
JB
174 HeartbeatRequest,
175 HeartbeatResponse
8ec8e3d0 176 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
9d73266c 177 ],
d3195f0a
JB
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
1969f643
JB
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 );
d3195f0a
JB
209 },
210 ],
91a7d3ea
JB
211 [
212 BroadcastChannelProcedureName.DATA_TRANSFER,
213 async (requestPayload?: BroadcastChannelRequestPayload) =>
214 this.chargingStation.ocppRequestService.requestHandler<
215 DataTransferRequest,
216 DataTransferResponse
8ec8e3d0 217 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
91a7d3ea 218 ],
c9a4f9ea
JB
219 [
220 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
221 async (requestPayload?: BroadcastChannelRequestPayload) =>
222 this.chargingStation.ocppRequestService.requestHandler<
223 DiagnosticsStatusNotificationRequest,
224 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
225 >(
226 this.chargingStation,
227 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
228 requestPayload,
229 requestParams
230 ),
c9a4f9ea
JB
231 ],
232 [
233 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
234 async (requestPayload?: BroadcastChannelRequestPayload) =>
235 this.chargingStation.ocppRequestService.requestHandler<
236 FirmwareStatusNotificationRequest,
237 FirmwareStatusNotificationResponse
8ec8e3d0
JB
238 >(
239 this.chargingStation,
240 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
241 requestPayload,
242 requestParams
243 ),
c9a4f9ea 244 ],
9d73266c 245 ]);
89b7a234 246 this.chargingStation = chargingStation;
02a6943a 247 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
6c8f5d90 248 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
89b7a234
JB
249 }
250
02a6943a 251 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
5dea4c94
JB
252 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
253 if (validatedMessageEvent === false) {
6c8f5d90
JB
254 return;
255 }
5dea4c94
JB
256 if (this.isResponse(validatedMessageEvent.data) === true) {
257 return;
258 }
259 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
2afb4d15 260 if (
9bb1159e 261 !Utils.isNullOrUndefined(requestPayload?.hashIds) &&
2afb4d15
JB
262 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
263 ) {
264 return;
265 }
9bb1159e 266 if (!Utils.isNullOrUndefined(requestPayload?.hashId)) {
2afb4d15 267 logger.error(
edd13439 268 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
2afb4d15
JB
269 );
270 return;
4eca248c 271 }
6c8f5d90 272 let responsePayload: BroadcastChannelResponsePayload;
9d73266c 273 let commandResponse: CommandResponse | void;
6c8f5d90
JB
274 try {
275 commandResponse = await this.commandHandler(command, requestPayload);
c9a4f9ea 276 if (
9bb1159e 277 Utils.isNullOrUndefined(commandResponse) ||
e92fdb8c 278 Utils.isEmptyObject(commandResponse as CommandResponse)
c9a4f9ea 279 ) {
10d244c0 280 responsePayload = {
51c83d6f 281 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
282 status: ResponseStatus.SUCCESS,
283 };
6c8f5d90 284 } else {
d3195f0a 285 responsePayload = this.commandResponseToResponsePayload(
1984f194 286 command,
d3195f0a 287 requestPayload,
e92fdb8c 288 commandResponse as CommandResponse
1984f194 289 );
6c8f5d90
JB
290 }
291 } catch (error) {
292 logger.error(
293 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
294 error
295 );
296 responsePayload = {
51c83d6f 297 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
298 status: ResponseStatus.FAILURE,
299 command,
300 requestPayload,
9d73266c 301 commandResponse: commandResponse as CommandResponse,
6c8f5d90
JB
302 errorMessage: (error as Error).message,
303 errorStack: (error as Error).stack,
a9ed42b2 304 errorDetails: (error as OCPPError).details,
6c8f5d90 305 };
623b39b5
JB
306 } finally {
307 this.sendResponse([uuid, responsePayload]);
6c8f5d90 308 }
6c8f5d90
JB
309 }
310
311 private messageErrorHandler(messageEvent: MessageEvent): void {
312 logger.error(
313 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
5083d31a 314 messageEvent
6c8f5d90
JB
315 );
316 }
317
318 private async commandHandler(
319 command: BroadcastChannelProcedureName,
320 requestPayload: BroadcastChannelRequestPayload
9d73266c
JB
321 ): Promise<CommandResponse | void> {
322 if (this.commandHandlers.has(command) === true) {
1984f194 323 this.cleanRequestPayload(command, requestPayload);
9d73266c 324 return this.commandHandlers.get(command)(requestPayload);
6c8f5d90 325 }
9d73266c 326 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
6c8f5d90
JB
327 }
328
1984f194
JB
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,
8f879946 338 ].includes(command) === false && delete requestPayload.connectorIds;
1984f194
JB
339 }
340
d3195f0a
JB
341 private commandResponseToResponsePayload(
342 command: BroadcastChannelProcedureName,
343 requestPayload: BroadcastChannelRequestPayload,
344 commandResponse: CommandResponse
345 ): BroadcastChannelResponsePayload {
cfa257f5
JB
346 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
347 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a
JB
348 return {
349 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 350 status: responseStatus,
d3195f0a
JB
351 };
352 }
353 return {
354 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 355 status: responseStatus,
d3195f0a
JB
356 command,
357 requestPayload,
358 commandResponse,
359 };
360 }
361
cfa257f5 362 private commandResponseToResponseStatus(
10db00b2
JB
363 command: BroadcastChannelProcedureName,
364 commandResponse: CommandResponse
365 ): ResponseStatus {
366 switch (command) {
367 case BroadcastChannelProcedureName.START_TRANSACTION:
368 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 369 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 370 if (
1984f194
JB
371 (
372 commandResponse as
373 | StartTransactionResponse
374 | StopTransactionResponse
375 | AuthorizeResponse
376 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2
JB
377 ) {
378 return ResponseStatus.SUCCESS;
379 }
380 return ResponseStatus.FAILURE;
8bfbc743 381 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
d270cc87 382 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
8bfbc743
JB
383 return ResponseStatus.SUCCESS;
384 }
385 return ResponseStatus.FAILURE;
91a7d3ea
JB
386 case BroadcastChannelProcedureName.DATA_TRANSFER:
387 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
388 return ResponseStatus.SUCCESS;
389 }
390 return ResponseStatus.FAILURE;
10db00b2 391 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 392 case BroadcastChannelProcedureName.METER_VALUES:
10db00b2
JB
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;
89b7a234
JB
404 }
405 }
406}