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