2697397a6b7e6bcebed8fd47cd34a548bb471c6e
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
1 import { secondsToMilliseconds } from 'date-fns';
2
3 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel';
4 import { BaseError, type OCPPError } from '../../exception';
5 import {
6 AuthorizationStatus,
7 type AuthorizeRequest,
8 type AuthorizeResponse,
9 type BootNotificationRequest,
10 type BootNotificationResponse,
11 BroadcastChannelProcedureName,
12 type BroadcastChannelRequest,
13 type BroadcastChannelRequestPayload,
14 type BroadcastChannelResponsePayload,
15 type DataTransferRequest,
16 type DataTransferResponse,
17 DataTransferStatus,
18 type DiagnosticsStatusNotificationRequest,
19 type DiagnosticsStatusNotificationResponse,
20 type EmptyObject,
21 type FirmwareStatusNotificationRequest,
22 type FirmwareStatusNotificationResponse,
23 type HeartbeatRequest,
24 type HeartbeatResponse,
25 type MessageEvent,
26 type MeterValuesRequest,
27 type MeterValuesResponse,
28 RegistrationStatusEnumType,
29 RequestCommand,
30 type RequestParams,
31 ResponseStatus,
32 StandardParametersKey,
33 type StartTransactionRequest,
34 type StartTransactionResponse,
35 type StatusNotificationRequest,
36 type StatusNotificationResponse,
37 type StopTransactionRequest,
38 type StopTransactionResponse,
39 } from '../../types';
40 import { Constants, convertToInt, isEmptyObject, isNullOrUndefined, logger } from '../../utils';
41 import type { ChargingStation } from '../ChargingStation';
42 import { getConfigurationKey } from '../ConfigurationKeyUtils';
43 import { buildMeterValue } from '../ocpp';
44
45 const moduleName = 'ChargingStationWorkerBroadcastChannel';
46
47 type CommandResponse =
48 | EmptyObject
49 | StartTransactionResponse
50 | StopTransactionResponse
51 | AuthorizeResponse
52 | BootNotificationResponse
53 | HeartbeatResponse
54 | DataTransferResponse;
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 = 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 buildMeterValue(
192 this.chargingStation,
193 requestPayload!.connectorId!,
194 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
195 .transactionId!,
196 configuredMeterValueSampleInterval !== undefined
197 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
198 : Constants.DEFAULT_METER_VALUES_INTERVAL,
199 ),
200 ],
201 ...requestPayload,
202 },
203 requestParams,
204 );
205 },
206 ],
207 [
208 BroadcastChannelProcedureName.DATA_TRANSFER,
209 async (requestPayload?: BroadcastChannelRequestPayload) =>
210 this.chargingStation.ocppRequestService.requestHandler<
211 DataTransferRequest,
212 DataTransferResponse
213 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
214 ],
215 [
216 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
217 async (requestPayload?: BroadcastChannelRequestPayload) =>
218 this.chargingStation.ocppRequestService.requestHandler<
219 DiagnosticsStatusNotificationRequest,
220 DiagnosticsStatusNotificationResponse
221 >(
222 this.chargingStation,
223 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
224 requestPayload,
225 requestParams,
226 ),
227 ],
228 [
229 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
230 async (requestPayload?: BroadcastChannelRequestPayload) =>
231 this.chargingStation.ocppRequestService.requestHandler<
232 FirmwareStatusNotificationRequest,
233 FirmwareStatusNotificationResponse
234 >(
235 this.chargingStation,
236 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
237 requestPayload,
238 requestParams,
239 ),
240 ],
241 ]);
242 this.chargingStation = chargingStation;
243 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void;
244 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void;
245 }
246
247 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
248 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
249 if (validatedMessageEvent === false) {
250 return;
251 }
252 if (this.isResponse(validatedMessageEvent.data) === true) {
253 return;
254 }
255 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
256 if (
257 !isNullOrUndefined(requestPayload.hashIds) &&
258 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
259 ) {
260 return;
261 }
262 if (!isNullOrUndefined(requestPayload.hashId)) {
263 logger.error(
264 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`,
265 );
266 return;
267 }
268 let responsePayload: BroadcastChannelResponsePayload | undefined;
269 let commandResponse: CommandResponse | void | undefined;
270 try {
271 commandResponse = await this.commandHandler(command, requestPayload);
272 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
273 responsePayload = {
274 hashId: this.chargingStation.stationInfo.hashId,
275 status: ResponseStatus.SUCCESS,
276 };
277 } else {
278 responsePayload = this.commandResponseToResponsePayload(
279 command,
280 requestPayload,
281 commandResponse as CommandResponse,
282 );
283 }
284 } catch (error) {
285 logger.error(
286 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
287 error,
288 );
289 responsePayload = {
290 hashId: this.chargingStation.stationInfo.hashId,
291 status: ResponseStatus.FAILURE,
292 command,
293 requestPayload,
294 commandResponse: commandResponse as CommandResponse,
295 errorMessage: (error as OCPPError).message,
296 errorStack: (error as OCPPError).stack,
297 errorDetails: (error as OCPPError).details,
298 };
299 } finally {
300 this.sendResponse([uuid, responsePayload!]);
301 }
302 }
303
304 private messageErrorHandler(messageEvent: MessageEvent): void {
305 logger.error(
306 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
307 messageEvent,
308 );
309 }
310
311 private async commandHandler(
312 command: BroadcastChannelProcedureName,
313 requestPayload: BroadcastChannelRequestPayload,
314 ): Promise<CommandResponse | void> {
315 if (this.commandHandlers.has(command) === true) {
316 this.cleanRequestPayload(command, requestPayload);
317 return this.commandHandlers.get(command)!(requestPayload);
318 }
319 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`);
320 }
321
322 private cleanRequestPayload(
323 command: BroadcastChannelProcedureName,
324 requestPayload: BroadcastChannelRequestPayload,
325 ): void {
326 delete requestPayload.hashId;
327 delete requestPayload.hashIds;
328 [
329 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
330 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
331 ].includes(command) === false && delete requestPayload.connectorIds;
332 }
333
334 private commandResponseToResponsePayload(
335 command: BroadcastChannelProcedureName,
336 requestPayload: BroadcastChannelRequestPayload,
337 commandResponse: CommandResponse,
338 ): BroadcastChannelResponsePayload {
339 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
340 if (responseStatus === ResponseStatus.SUCCESS) {
341 return {
342 hashId: this.chargingStation.stationInfo.hashId,
343 status: responseStatus,
344 };
345 }
346 return {
347 hashId: this.chargingStation.stationInfo.hashId,
348 status: responseStatus,
349 command,
350 requestPayload,
351 commandResponse,
352 };
353 }
354
355 private commandResponseToResponseStatus(
356 command: BroadcastChannelProcedureName,
357 commandResponse: CommandResponse,
358 ): ResponseStatus {
359 switch (command) {
360 case BroadcastChannelProcedureName.START_TRANSACTION:
361 case BroadcastChannelProcedureName.STOP_TRANSACTION:
362 case BroadcastChannelProcedureName.AUTHORIZE:
363 if (
364 (
365 commandResponse as
366 | StartTransactionResponse
367 | StopTransactionResponse
368 | AuthorizeResponse
369 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
370 ) {
371 return ResponseStatus.SUCCESS;
372 }
373 return ResponseStatus.FAILURE;
374 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
375 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
376 return ResponseStatus.SUCCESS;
377 }
378 return ResponseStatus.FAILURE;
379 case BroadcastChannelProcedureName.DATA_TRANSFER:
380 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
381 return ResponseStatus.SUCCESS;
382 }
383 return ResponseStatus.FAILURE;
384 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
385 case BroadcastChannelProcedureName.METER_VALUES:
386 if (isEmptyObject(commandResponse) === true) {
387 return ResponseStatus.SUCCESS;
388 }
389 return ResponseStatus.FAILURE;
390 case BroadcastChannelProcedureName.HEARTBEAT:
391 if ('currentTime' in commandResponse) {
392 return ResponseStatus.SUCCESS;
393 }
394 return ResponseStatus.FAILURE;
395 default:
396 return ResponseStatus.FAILURE;
397 }
398 }
399 }