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