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