02febe4ab81d3a275d0b5a216dfe7f172f622eb0
[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 } from '../utils/Constants';
43 import { logger } from '../utils/Logger';
44 import { Utils } from '../utils/Utils';
45
46 const moduleName = 'ChargingStationWorkerBroadcastChannel';
47
48 type CommandResponse =
49 | StartTransactionResponse
50 | StopTransactionResponse
51 | AuthorizeResponse
52 | BootNotificationResponse
53 | StatusNotificationResponse
54 | HeartbeatResponse
55 | MeterValuesResponse
56 | DataTransferResponse
57 | DiagnosticsStatusNotificationResponse
58 | FirmwareStatusNotificationResponse;
59
60 type CommandHandler = (
61 requestPayload?: BroadcastChannelRequestPayload
62 ) => Promise<CommandResponse | void> | void;
63
64 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
65 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
66 private readonly chargingStation: ChargingStation;
67
68 constructor(chargingStation: ChargingStation) {
69 super();
70 const requestParams: RequestParams = {
71 throwError: true,
72 };
73 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
74 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
75 [
76 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
77 async () => this.chargingStation.stop(),
78 ],
79 [
80 BroadcastChannelProcedureName.OPEN_CONNECTION,
81 () => this.chargingStation.openWSConnection(),
82 ],
83 [
84 BroadcastChannelProcedureName.CLOSE_CONNECTION,
85 () => this.chargingStation.closeWSConnection(),
86 ],
87 [
88 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
89 (requestPayload?: BroadcastChannelRequestPayload) =>
90 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds),
91 ],
92 [
93 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
94 (requestPayload?: BroadcastChannelRequestPayload) =>
95 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds),
96 ],
97 [
98 BroadcastChannelProcedureName.START_TRANSACTION,
99 async (requestPayload?: BroadcastChannelRequestPayload) =>
100 this.chargingStation.ocppRequestService.requestHandler<
101 StartTransactionRequest,
102 StartTransactionResponse
103 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
104 ],
105 [
106 BroadcastChannelProcedureName.STOP_TRANSACTION,
107 async (requestPayload?: BroadcastChannelRequestPayload) =>
108 this.chargingStation.ocppRequestService.requestHandler<
109 StopTransactionRequest,
110 StartTransactionResponse
111 >(
112 this.chargingStation,
113 RequestCommand.STOP_TRANSACTION,
114 {
115 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
116 requestPayload.transactionId,
117 true
118 ),
119 ...requestPayload,
120 },
121 requestParams
122 ),
123 ],
124 [
125 BroadcastChannelProcedureName.AUTHORIZE,
126 async (requestPayload?: BroadcastChannelRequestPayload) =>
127 this.chargingStation.ocppRequestService.requestHandler<
128 AuthorizeRequest,
129 AuthorizeResponse
130 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
131 ],
132 [
133 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
134 async (requestPayload?: BroadcastChannelRequestPayload) => {
135 this.chargingStation.bootNotificationResponse =
136 await this.chargingStation.ocppRequestService.requestHandler<
137 BootNotificationRequest,
138 BootNotificationResponse
139 >(
140 this.chargingStation,
141 RequestCommand.BOOT_NOTIFICATION,
142 {
143 ...this.chargingStation.bootNotificationRequest,
144 ...requestPayload,
145 },
146 {
147 skipBufferingOnError: true,
148 throwError: true,
149 }
150 );
151 return this.chargingStation.bootNotificationResponse;
152 },
153 ],
154 [
155 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
156 async (requestPayload?: BroadcastChannelRequestPayload) =>
157 this.chargingStation.ocppRequestService.requestHandler<
158 StatusNotificationRequest,
159 StatusNotificationResponse
160 >(
161 this.chargingStation,
162 RequestCommand.STATUS_NOTIFICATION,
163 requestPayload,
164 requestParams
165 ),
166 ],
167 [
168 BroadcastChannelProcedureName.HEARTBEAT,
169 async (requestPayload?: BroadcastChannelRequestPayload) =>
170 this.chargingStation.ocppRequestService.requestHandler<
171 HeartbeatRequest,
172 HeartbeatResponse
173 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
174 ],
175 [
176 BroadcastChannelProcedureName.METER_VALUES,
177 async (requestPayload?: BroadcastChannelRequestPayload) => {
178 const configuredMeterValueSampleInterval =
179 ChargingStationConfigurationUtils.getConfigurationKey(
180 chargingStation,
181 StandardParametersKey.MeterValueSampleInterval
182 );
183 return this.chargingStation.ocppRequestService.requestHandler<
184 MeterValuesRequest,
185 MeterValuesResponse
186 >(
187 this.chargingStation,
188 RequestCommand.METER_VALUES,
189 {
190 meterValue: [
191 // FIXME: Implement OCPP version agnostic helpers
192 OCPP16ServiceUtils.buildMeterValue(
193 this.chargingStation,
194 requestPayload.connectorId,
195 this.chargingStation.getConnectorStatus(requestPayload.connectorId)
196 ?.transactionId,
197 configuredMeterValueSampleInterval
198 ? Utils.convertToInt(configuredMeterValueSampleInterval.value) * 1000
199 : Constants.DEFAULT_METER_VALUES_INTERVAL
200 ),
201 ],
202 ...requestPayload,
203 },
204 requestParams
205 );
206 },
207 ],
208 [
209 BroadcastChannelProcedureName.DATA_TRANSFER,
210 async (requestPayload?: BroadcastChannelRequestPayload) =>
211 this.chargingStation.ocppRequestService.requestHandler<
212 DataTransferRequest,
213 DataTransferResponse
214 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
215 ],
216 [
217 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
218 async (requestPayload?: BroadcastChannelRequestPayload) =>
219 this.chargingStation.ocppRequestService.requestHandler<
220 DiagnosticsStatusNotificationRequest,
221 DiagnosticsStatusNotificationResponse
222 >(
223 this.chargingStation,
224 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
225 requestPayload,
226 requestParams
227 ),
228 ],
229 [
230 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
231 async (requestPayload?: BroadcastChannelRequestPayload) =>
232 this.chargingStation.ocppRequestService.requestHandler<
233 FirmwareStatusNotificationRequest,
234 FirmwareStatusNotificationResponse
235 >(
236 this.chargingStation,
237 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
238 requestPayload,
239 requestParams
240 ),
241 ],
242 ]);
243 this.chargingStation = chargingStation;
244 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
245 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
246 }
247
248 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
249 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
250 if (validatedMessageEvent === false) {
251 return;
252 }
253 if (this.isResponse(validatedMessageEvent.data) === true) {
254 return;
255 }
256 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
257 if (
258 requestPayload?.hashIds !== undefined &&
259 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
260 ) {
261 return;
262 }
263 if (requestPayload?.hashId !== undefined) {
264 logger.error(
265 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
266 );
267 return;
268 }
269 let responsePayload: BroadcastChannelResponsePayload;
270 let commandResponse: CommandResponse | void;
271 try {
272 commandResponse = await this.commandHandler(command, requestPayload);
273 if (
274 commandResponse === undefined ||
275 commandResponse === null ||
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 }