refactor(simulator): factor out common helpers
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
4c3c0d59 1import { WorkerBroadcastChannel } from './WorkerBroadcastChannel';
7671fa0b 2import { BaseError, type OCPPError } from '../../exception';
10db00b2 3import {
268a74bb
JB
4 AuthorizationStatus,
5 type AuthorizeRequest,
6 type AuthorizeResponse,
8bfbc743 7 type BootNotificationRequest,
8bfbc743 8 type BootNotificationResponse,
268a74bb
JB
9 BroadcastChannelProcedureName,
10 type BroadcastChannelRequest,
11 type BroadcastChannelRequestPayload,
12 type BroadcastChannelResponsePayload,
13 type DataTransferRequest,
91a7d3ea
JB
14 type DataTransferResponse,
15 DataTransferStatus,
268a74bb 16 type DiagnosticsStatusNotificationRequest,
c9a4f9ea 17 type DiagnosticsStatusNotificationResponse,
268a74bb 18 type FirmwareStatusNotificationRequest,
c9a4f9ea 19 type FirmwareStatusNotificationResponse,
268a74bb 20 type HeartbeatRequest,
8bfbc743 21 type HeartbeatResponse,
268a74bb
JB
22 type MessageEvent,
23 type MeterValuesRequest,
8bfbc743 24 type MeterValuesResponse,
d270cc87 25 RegistrationStatusEnumType,
268a74bb
JB
26 RequestCommand,
27 type RequestParams,
28 ResponseStatus,
29 StandardParametersKey,
8bfbc743
JB
30 type StartTransactionRequest,
31 type StartTransactionResponse,
268a74bb
JB
32 type StatusNotificationRequest,
33 type StatusNotificationResponse,
8bfbc743
JB
34 type StopTransactionRequest,
35 type StopTransactionResponse,
7671fa0b
JB
36} from '../../types';
37import { Constants, Utils, logger } from '../../utils';
38import type { ChargingStation } from '../ChargingStation';
39import { ChargingStationConfigurationUtils } from '../ChargingStationConfigurationUtils';
40import { OCPP16ServiceUtils } from '../ocpp';
89b7a234 41
4e3ff94d
JB
42const moduleName = 'ChargingStationWorkerBroadcastChannel';
43
a9ed42b2
JB
44type CommandResponse =
45 | StartTransactionResponse
46 | StopTransactionResponse
1984f194 47 | AuthorizeResponse
8bfbc743 48 | BootNotificationResponse
10db00b2 49 | StatusNotificationResponse
d3195f0a 50 | HeartbeatResponse
91a7d3ea 51 | MeterValuesResponse
c9a4f9ea
JB
52 | DataTransferResponse
53 | DiagnosticsStatusNotificationResponse
54 | FirmwareStatusNotificationResponse;
89b7a234 55
d273692c
JB
56type CommandHandler = (
57 requestPayload?: BroadcastChannelRequestPayload
58) => Promise<CommandResponse | void> | void;
59
268a74bb 60export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
d273692c 61 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
89b7a234
JB
62 private readonly chargingStation: ChargingStation;
63
64 constructor(chargingStation: ChargingStation) {
1598b27c 65 super();
8ec8e3d0
JB
66 const requestParams: RequestParams = {
67 throwError: true,
68 };
d273692c 69 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
9d73266c
JB
70 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
71 [
72 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
d273692c 73 async () => this.chargingStation.stop(),
9d73266c
JB
74 ],
75 [
76 BroadcastChannelProcedureName.OPEN_CONNECTION,
77 () => this.chargingStation.openWSConnection(),
78 ],
79 [
80 BroadcastChannelProcedureName.CLOSE_CONNECTION,
81 () => this.chargingStation.closeWSConnection(),
82 ],
623b39b5
JB
83 [
84 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
85 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 86 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5
JB
87 ],
88 [
89 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
90 (requestPayload?: BroadcastChannelRequestPayload) =>
72092cfc 91 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds),
623b39b5 92 ],
269de583
JB
93 [
94 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
95 (requestPayload?: BroadcastChannelRequestPayload) =>
96 this.chargingStation.setSupervisionUrl(requestPayload?.url as string),
97 ],
9d73266c
JB
98 [
99 BroadcastChannelProcedureName.START_TRANSACTION,
100 async (requestPayload?: BroadcastChannelRequestPayload) =>
101 this.chargingStation.ocppRequestService.requestHandler<
102 StartTransactionRequest,
103 StartTransactionResponse
8ec8e3d0 104 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
9d73266c
JB
105 ],
106 [
107 BroadcastChannelProcedureName.STOP_TRANSACTION,
108 async (requestPayload?: BroadcastChannelRequestPayload) =>
109 this.chargingStation.ocppRequestService.requestHandler<
110 StopTransactionRequest,
111 StartTransactionResponse
1969f643
JB
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 ),
9d73266c 124 ],
1984f194
JB
125 [
126 BroadcastChannelProcedureName.AUTHORIZE,
127 async (requestPayload?: BroadcastChannelRequestPayload) =>
128 this.chargingStation.ocppRequestService.requestHandler<
129 AuthorizeRequest,
130 AuthorizeResponse
8ec8e3d0 131 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
1984f194 132 ],
8bfbc743
JB
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,
8ec8e3d0 149 throwError: true,
8bfbc743
JB
150 }
151 );
152 return this.chargingStation.bootNotificationResponse;
153 },
154 ],
9d73266c
JB
155 [
156 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
157 async (requestPayload?: BroadcastChannelRequestPayload) =>
158 this.chargingStation.ocppRequestService.requestHandler<
159 StatusNotificationRequest,
160 StatusNotificationResponse
8ec8e3d0
JB
161 >(
162 this.chargingStation,
163 RequestCommand.STATUS_NOTIFICATION,
164 requestPayload,
165 requestParams
166 ),
9d73266c
JB
167 ],
168 [
169 BroadcastChannelProcedureName.HEARTBEAT,
1984f194
JB
170 async (requestPayload?: BroadcastChannelRequestPayload) =>
171 this.chargingStation.ocppRequestService.requestHandler<
9d73266c
JB
172 HeartbeatRequest,
173 HeartbeatResponse
8ec8e3d0 174 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
9d73266c 175 ],
d3195f0a
JB
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
1969f643
JB
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 );
d3195f0a
JB
207 },
208 ],
91a7d3ea
JB
209 [
210 BroadcastChannelProcedureName.DATA_TRANSFER,
211 async (requestPayload?: BroadcastChannelRequestPayload) =>
212 this.chargingStation.ocppRequestService.requestHandler<
213 DataTransferRequest,
214 DataTransferResponse
8ec8e3d0 215 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
91a7d3ea 216 ],
c9a4f9ea
JB
217 [
218 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
219 async (requestPayload?: BroadcastChannelRequestPayload) =>
220 this.chargingStation.ocppRequestService.requestHandler<
221 DiagnosticsStatusNotificationRequest,
222 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
223 >(
224 this.chargingStation,
225 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
226 requestPayload,
227 requestParams
228 ),
c9a4f9ea
JB
229 ],
230 [
231 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
232 async (requestPayload?: BroadcastChannelRequestPayload) =>
233 this.chargingStation.ocppRequestService.requestHandler<
234 FirmwareStatusNotificationRequest,
235 FirmwareStatusNotificationResponse
8ec8e3d0
JB
236 >(
237 this.chargingStation,
238 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
239 requestPayload,
240 requestParams
241 ),
c9a4f9ea 242 ],
9d73266c 243 ]);
89b7a234 244 this.chargingStation = chargingStation;
02a6943a 245 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
6c8f5d90 246 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
89b7a234
JB
247 }
248
02a6943a 249 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
5dea4c94
JB
250 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
251 if (validatedMessageEvent === false) {
6c8f5d90
JB
252 return;
253 }
5dea4c94
JB
254 if (this.isResponse(validatedMessageEvent.data) === true) {
255 return;
256 }
257 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
2afb4d15 258 if (
9bb1159e 259 !Utils.isNullOrUndefined(requestPayload?.hashIds) &&
2afb4d15
JB
260 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
261 ) {
262 return;
263 }
9bb1159e 264 if (!Utils.isNullOrUndefined(requestPayload?.hashId)) {
2afb4d15 265 logger.error(
edd13439 266 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`
2afb4d15
JB
267 );
268 return;
4eca248c 269 }
6c8f5d90 270 let responsePayload: BroadcastChannelResponsePayload;
9d73266c 271 let commandResponse: CommandResponse | void;
6c8f5d90
JB
272 try {
273 commandResponse = await this.commandHandler(command, requestPayload);
c9a4f9ea 274 if (
9bb1159e 275 Utils.isNullOrUndefined(commandResponse) ||
e92fdb8c 276 Utils.isEmptyObject(commandResponse as CommandResponse)
c9a4f9ea 277 ) {
10d244c0 278 responsePayload = {
51c83d6f 279 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
280 status: ResponseStatus.SUCCESS,
281 };
6c8f5d90 282 } else {
d3195f0a 283 responsePayload = this.commandResponseToResponsePayload(
1984f194 284 command,
d3195f0a 285 requestPayload,
e92fdb8c 286 commandResponse as CommandResponse
1984f194 287 );
6c8f5d90
JB
288 }
289 } catch (error) {
290 logger.error(
291 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
292 error
293 );
294 responsePayload = {
51c83d6f 295 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
296 status: ResponseStatus.FAILURE,
297 command,
298 requestPayload,
9d73266c 299 commandResponse: commandResponse as CommandResponse,
6c8f5d90
JB
300 errorMessage: (error as Error).message,
301 errorStack: (error as Error).stack,
a9ed42b2 302 errorDetails: (error as OCPPError).details,
6c8f5d90 303 };
623b39b5
JB
304 } finally {
305 this.sendResponse([uuid, responsePayload]);
6c8f5d90 306 }
6c8f5d90
JB
307 }
308
309 private messageErrorHandler(messageEvent: MessageEvent): void {
310 logger.error(
311 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
5083d31a 312 messageEvent
6c8f5d90
JB
313 );
314 }
315
316 private async commandHandler(
317 command: BroadcastChannelProcedureName,
318 requestPayload: BroadcastChannelRequestPayload
9d73266c
JB
319 ): Promise<CommandResponse | void> {
320 if (this.commandHandlers.has(command) === true) {
1984f194 321 this.cleanRequestPayload(command, requestPayload);
9d73266c 322 return this.commandHandlers.get(command)(requestPayload);
6c8f5d90 323 }
9d73266c 324 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
6c8f5d90
JB
325 }
326
1984f194
JB
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,
8f879946 336 ].includes(command) === false && delete requestPayload.connectorIds;
1984f194
JB
337 }
338
d3195f0a
JB
339 private commandResponseToResponsePayload(
340 command: BroadcastChannelProcedureName,
341 requestPayload: BroadcastChannelRequestPayload,
342 commandResponse: CommandResponse
343 ): BroadcastChannelResponsePayload {
cfa257f5
JB
344 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
345 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a
JB
346 return {
347 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 348 status: responseStatus,
d3195f0a
JB
349 };
350 }
351 return {
352 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 353 status: responseStatus,
d3195f0a
JB
354 command,
355 requestPayload,
356 commandResponse,
357 };
358 }
359
cfa257f5 360 private commandResponseToResponseStatus(
10db00b2
JB
361 command: BroadcastChannelProcedureName,
362 commandResponse: CommandResponse
363 ): ResponseStatus {
364 switch (command) {
365 case BroadcastChannelProcedureName.START_TRANSACTION:
366 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 367 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 368 if (
1984f194
JB
369 (
370 commandResponse as
371 | StartTransactionResponse
372 | StopTransactionResponse
373 | AuthorizeResponse
374 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2
JB
375 ) {
376 return ResponseStatus.SUCCESS;
377 }
378 return ResponseStatus.FAILURE;
8bfbc743 379 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
d270cc87 380 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
8bfbc743
JB
381 return ResponseStatus.SUCCESS;
382 }
383 return ResponseStatus.FAILURE;
91a7d3ea
JB
384 case BroadcastChannelProcedureName.DATA_TRANSFER:
385 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
386 return ResponseStatus.SUCCESS;
387 }
388 return ResponseStatus.FAILURE;
10db00b2 389 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 390 case BroadcastChannelProcedureName.METER_VALUES:
10db00b2
JB
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;
89b7a234
JB
402 }
403 }
404}