fix: ensure running transactions are stopped at CS stop
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
CommitLineData
be4c6702
JB
1import { secondsToMilliseconds } from 'date-fns';
2
4c3c0d59 3import { WorkerBroadcastChannel } from './WorkerBroadcastChannel';
7671fa0b 4import { BaseError, type OCPPError } from '../../exception';
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,
7671fa0b 39} from '../../types';
9bf0ef23 40import { Constants, convertToInt, isEmptyObject, isNullOrUndefined, logger } from '../../utils';
7671fa0b 41import type { ChargingStation } from '../ChargingStation';
357a5553 42import { getConfigurationKey } from '../ConfigurationKeyUtils';
7671fa0b 43import { OCPP16ServiceUtils } from '../ocpp';
89b7a234 44
4e3ff94d
JB
45const moduleName = 'ChargingStationWorkerBroadcastChannel';
46
a9ed42b2 47type CommandResponse =
346b47e0 48 | EmptyObject
a9ed42b2
JB
49 | StartTransactionResponse
50 | StopTransactionResponse
1984f194 51 | AuthorizeResponse
8bfbc743 52 | BootNotificationResponse
d3195f0a 53 | HeartbeatResponse
e1d9a0f4 54 | DataTransferResponse;
89b7a234 55
d273692c 56type CommandHandler = (
5edd8ba0 57 requestPayload?: BroadcastChannelRequestPayload,
d273692c
JB
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,
9ff486f4
JB
90 (requestPayload?: BroadcastChannelRequestPayload) =>
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(
e1d9a0f4 117 requestPayload!.transactionId!,
5edd8ba0 118 true,
1969f643
JB
119 ),
120 ...requestPayload,
121 },
5edd8ba0 122 requestParams,
1969f643 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,
5edd8ba0 150 },
8bfbc743
JB
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,
5edd8ba0 165 requestParams,
8ec8e3d0 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) => {
f2d5e3d9
JB
179 const configuredMeterValueSampleInterval = getConfigurationKey(
180 chargingStation,
181 StandardParametersKey.MeterValueSampleInterval,
182 );
d3195f0a
JB
183 return this.chargingStation.ocppRequestService.requestHandler<
184 MeterValuesRequest,
185 MeterValuesResponse
1969f643
JB
186 >(
187 this.chargingStation,
188 RequestCommand.METER_VALUES,
189 {
190 meterValue: [
191 // FIXME: Implement OCPP version agnostic helpers
192 OCPP16ServiceUtils.buildMeterValue(
193 this.chargingStation,
e1d9a0f4
JB
194 requestPayload!.connectorId!,
195 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
196 .transactionId!,
1969f643 197 configuredMeterValueSampleInterval
be4c6702 198 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
5edd8ba0 199 : Constants.DEFAULT_METER_VALUES_INTERVAL,
1969f643
JB
200 ),
201 ],
202 ...requestPayload,
203 },
5edd8ba0 204 requestParams,
1969f643 205 );
d3195f0a
JB
206 },
207 ],
91a7d3ea
JB
208 [
209 BroadcastChannelProcedureName.DATA_TRANSFER,
210 async (requestPayload?: BroadcastChannelRequestPayload) =>
211 this.chargingStation.ocppRequestService.requestHandler<
212 DataTransferRequest,
213 DataTransferResponse
8ec8e3d0 214 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
91a7d3ea 215 ],
c9a4f9ea
JB
216 [
217 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
218 async (requestPayload?: BroadcastChannelRequestPayload) =>
219 this.chargingStation.ocppRequestService.requestHandler<
220 DiagnosticsStatusNotificationRequest,
221 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
222 >(
223 this.chargingStation,
224 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
225 requestPayload,
5edd8ba0 226 requestParams,
8ec8e3d0 227 ),
c9a4f9ea
JB
228 ],
229 [
230 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
231 async (requestPayload?: BroadcastChannelRequestPayload) =>
232 this.chargingStation.ocppRequestService.requestHandler<
233 FirmwareStatusNotificationRequest,
234 FirmwareStatusNotificationResponse
8ec8e3d0
JB
235 >(
236 this.chargingStation,
237 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
238 requestPayload,
5edd8ba0 239 requestParams,
8ec8e3d0 240 ),
c9a4f9ea 241 ],
9d73266c 242 ]);
89b7a234 243 this.chargingStation = chargingStation;
a37fc6dc
JB
244 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void;
245 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void;
89b7a234
JB
246 }
247
02a6943a 248 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
5dea4c94
JB
249 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
250 if (validatedMessageEvent === false) {
6c8f5d90
JB
251 return;
252 }
5dea4c94
JB
253 if (this.isResponse(validatedMessageEvent.data) === true) {
254 return;
255 }
256 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
2afb4d15 257 if (
4a3807d1
JB
258 !isNullOrUndefined(requestPayload.hashIds) &&
259 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
2afb4d15
JB
260 ) {
261 return;
262 }
4a3807d1 263 if (!isNullOrUndefined(requestPayload.hashId)) {
2afb4d15 264 logger.error(
5edd8ba0 265 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`,
2afb4d15
JB
266 );
267 return;
4eca248c 268 }
e1d9a0f4
JB
269 let responsePayload: BroadcastChannelResponsePayload | undefined;
270 let commandResponse: CommandResponse | void | undefined;
6c8f5d90
JB
271 try {
272 commandResponse = await this.commandHandler(command, requestPayload);
9bf0ef23 273 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
10d244c0 274 responsePayload = {
51c83d6f 275 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
276 status: ResponseStatus.SUCCESS,
277 };
6c8f5d90 278 } else {
d3195f0a 279 responsePayload = this.commandResponseToResponsePayload(
1984f194 280 command,
d3195f0a 281 requestPayload,
5edd8ba0 282 commandResponse as CommandResponse,
1984f194 283 );
6c8f5d90
JB
284 }
285 } catch (error) {
286 logger.error(
287 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
5edd8ba0 288 error,
6c8f5d90
JB
289 );
290 responsePayload = {
51c83d6f 291 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
292 status: ResponseStatus.FAILURE,
293 command,
294 requestPayload,
9d73266c 295 commandResponse: commandResponse as CommandResponse,
6c8f5d90
JB
296 errorMessage: (error as Error).message,
297 errorStack: (error as Error).stack,
a9ed42b2 298 errorDetails: (error as OCPPError).details,
6c8f5d90 299 };
623b39b5 300 } finally {
e1d9a0f4 301 this.sendResponse([uuid, responsePayload!]);
6c8f5d90 302 }
6c8f5d90
JB
303 }
304
305 private messageErrorHandler(messageEvent: MessageEvent): void {
306 logger.error(
307 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
5edd8ba0 308 messageEvent,
6c8f5d90
JB
309 );
310 }
311
312 private async commandHandler(
313 command: BroadcastChannelProcedureName,
5edd8ba0 314 requestPayload: BroadcastChannelRequestPayload,
9d73266c
JB
315 ): Promise<CommandResponse | void> {
316 if (this.commandHandlers.has(command) === true) {
1984f194 317 this.cleanRequestPayload(command, requestPayload);
e1d9a0f4 318 return this.commandHandlers.get(command)!(requestPayload);
6c8f5d90 319 }
90aceaf6 320 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`);
6c8f5d90
JB
321 }
322
1984f194
JB
323 private cleanRequestPayload(
324 command: BroadcastChannelProcedureName,
5edd8ba0 325 requestPayload: BroadcastChannelRequestPayload,
1984f194
JB
326 ): void {
327 delete requestPayload.hashId;
328 delete requestPayload.hashIds;
329 [
330 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
331 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
8f879946 332 ].includes(command) === false && delete requestPayload.connectorIds;
1984f194
JB
333 }
334
d3195f0a
JB
335 private commandResponseToResponsePayload(
336 command: BroadcastChannelProcedureName,
337 requestPayload: BroadcastChannelRequestPayload,
5edd8ba0 338 commandResponse: CommandResponse,
d3195f0a 339 ): BroadcastChannelResponsePayload {
cfa257f5
JB
340 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
341 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a
JB
342 return {
343 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 344 status: responseStatus,
d3195f0a
JB
345 };
346 }
347 return {
348 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 349 status: responseStatus,
d3195f0a
JB
350 command,
351 requestPayload,
352 commandResponse,
353 };
354 }
355
cfa257f5 356 private commandResponseToResponseStatus(
10db00b2 357 command: BroadcastChannelProcedureName,
5edd8ba0 358 commandResponse: CommandResponse,
10db00b2
JB
359 ): ResponseStatus {
360 switch (command) {
361 case BroadcastChannelProcedureName.START_TRANSACTION:
362 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 363 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 364 if (
1984f194
JB
365 (
366 commandResponse as
367 | StartTransactionResponse
368 | StopTransactionResponse
369 | AuthorizeResponse
370 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2
JB
371 ) {
372 return ResponseStatus.SUCCESS;
373 }
374 return ResponseStatus.FAILURE;
8bfbc743 375 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
d270cc87 376 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
8bfbc743
JB
377 return ResponseStatus.SUCCESS;
378 }
379 return ResponseStatus.FAILURE;
91a7d3ea
JB
380 case BroadcastChannelProcedureName.DATA_TRANSFER:
381 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
382 return ResponseStatus.SUCCESS;
383 }
384 return ResponseStatus.FAILURE;
10db00b2 385 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 386 case BroadcastChannelProcedureName.METER_VALUES:
9bf0ef23 387 if (isEmptyObject(commandResponse) === true) {
10db00b2
JB
388 return ResponseStatus.SUCCESS;
389 }
390 return ResponseStatus.FAILURE;
391 case BroadcastChannelProcedureName.HEARTBEAT:
392 if ('currentTime' in commandResponse) {
393 return ResponseStatus.SUCCESS;
394 }
395 return ResponseStatus.FAILURE;
396 default:
397 return ResponseStatus.FAILURE;
89b7a234
JB
398 }
399 }
400}