fix: ensure stop transaction is sent at ATG 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,
49563992
JB
90 async (requestPayload?: BroadcastChannelRequestPayload) =>
91 await this.chargingStation.stopAutomaticTransactionGenerator(
92 requestPayload?.connectorIds,
93 ),
623b39b5 94 ],
269de583
JB
95 [
96 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
97 (requestPayload?: BroadcastChannelRequestPayload) =>
98 this.chargingStation.setSupervisionUrl(requestPayload?.url as string),
99 ],
9d73266c
JB
100 [
101 BroadcastChannelProcedureName.START_TRANSACTION,
102 async (requestPayload?: BroadcastChannelRequestPayload) =>
103 this.chargingStation.ocppRequestService.requestHandler<
104 StartTransactionRequest,
105 StartTransactionResponse
8ec8e3d0 106 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
9d73266c
JB
107 ],
108 [
109 BroadcastChannelProcedureName.STOP_TRANSACTION,
110 async (requestPayload?: BroadcastChannelRequestPayload) =>
111 this.chargingStation.ocppRequestService.requestHandler<
112 StopTransactionRequest,
113 StartTransactionResponse
1969f643
JB
114 >(
115 this.chargingStation,
116 RequestCommand.STOP_TRANSACTION,
117 {
118 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
e1d9a0f4 119 requestPayload!.transactionId!,
5edd8ba0 120 true,
1969f643
JB
121 ),
122 ...requestPayload,
123 },
5edd8ba0 124 requestParams,
1969f643 125 ),
9d73266c 126 ],
1984f194
JB
127 [
128 BroadcastChannelProcedureName.AUTHORIZE,
129 async (requestPayload?: BroadcastChannelRequestPayload) =>
130 this.chargingStation.ocppRequestService.requestHandler<
131 AuthorizeRequest,
132 AuthorizeResponse
8ec8e3d0 133 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
1984f194 134 ],
8bfbc743
JB
135 [
136 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
137 async (requestPayload?: BroadcastChannelRequestPayload) => {
138 this.chargingStation.bootNotificationResponse =
139 await this.chargingStation.ocppRequestService.requestHandler<
140 BootNotificationRequest,
141 BootNotificationResponse
142 >(
143 this.chargingStation,
144 RequestCommand.BOOT_NOTIFICATION,
145 {
146 ...this.chargingStation.bootNotificationRequest,
147 ...requestPayload,
148 },
149 {
150 skipBufferingOnError: true,
8ec8e3d0 151 throwError: true,
5edd8ba0 152 },
8bfbc743
JB
153 );
154 return this.chargingStation.bootNotificationResponse;
155 },
156 ],
9d73266c
JB
157 [
158 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
159 async (requestPayload?: BroadcastChannelRequestPayload) =>
160 this.chargingStation.ocppRequestService.requestHandler<
161 StatusNotificationRequest,
162 StatusNotificationResponse
8ec8e3d0
JB
163 >(
164 this.chargingStation,
165 RequestCommand.STATUS_NOTIFICATION,
166 requestPayload,
5edd8ba0 167 requestParams,
8ec8e3d0 168 ),
9d73266c
JB
169 ],
170 [
171 BroadcastChannelProcedureName.HEARTBEAT,
1984f194
JB
172 async (requestPayload?: BroadcastChannelRequestPayload) =>
173 this.chargingStation.ocppRequestService.requestHandler<
9d73266c
JB
174 HeartbeatRequest,
175 HeartbeatResponse
8ec8e3d0 176 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
9d73266c 177 ],
d3195f0a
JB
178 [
179 BroadcastChannelProcedureName.METER_VALUES,
180 async (requestPayload?: BroadcastChannelRequestPayload) => {
f2d5e3d9
JB
181 const configuredMeterValueSampleInterval = getConfigurationKey(
182 chargingStation,
183 StandardParametersKey.MeterValueSampleInterval,
184 );
d3195f0a
JB
185 return this.chargingStation.ocppRequestService.requestHandler<
186 MeterValuesRequest,
187 MeterValuesResponse
1969f643
JB
188 >(
189 this.chargingStation,
190 RequestCommand.METER_VALUES,
191 {
192 meterValue: [
193 // FIXME: Implement OCPP version agnostic helpers
194 OCPP16ServiceUtils.buildMeterValue(
195 this.chargingStation,
e1d9a0f4
JB
196 requestPayload!.connectorId!,
197 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
198 .transactionId!,
1969f643 199 configuredMeterValueSampleInterval
be4c6702 200 ? secondsToMilliseconds(convertToInt(configuredMeterValueSampleInterval.value))
5edd8ba0 201 : Constants.DEFAULT_METER_VALUES_INTERVAL,
1969f643
JB
202 ),
203 ],
204 ...requestPayload,
205 },
5edd8ba0 206 requestParams,
1969f643 207 );
d3195f0a
JB
208 },
209 ],
91a7d3ea
JB
210 [
211 BroadcastChannelProcedureName.DATA_TRANSFER,
212 async (requestPayload?: BroadcastChannelRequestPayload) =>
213 this.chargingStation.ocppRequestService.requestHandler<
214 DataTransferRequest,
215 DataTransferResponse
8ec8e3d0 216 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
91a7d3ea 217 ],
c9a4f9ea
JB
218 [
219 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
220 async (requestPayload?: BroadcastChannelRequestPayload) =>
221 this.chargingStation.ocppRequestService.requestHandler<
222 DiagnosticsStatusNotificationRequest,
223 DiagnosticsStatusNotificationResponse
8ec8e3d0
JB
224 >(
225 this.chargingStation,
226 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
227 requestPayload,
5edd8ba0 228 requestParams,
8ec8e3d0 229 ),
c9a4f9ea
JB
230 ],
231 [
232 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
233 async (requestPayload?: BroadcastChannelRequestPayload) =>
234 this.chargingStation.ocppRequestService.requestHandler<
235 FirmwareStatusNotificationRequest,
236 FirmwareStatusNotificationResponse
8ec8e3d0
JB
237 >(
238 this.chargingStation,
239 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
240 requestPayload,
5edd8ba0 241 requestParams,
8ec8e3d0 242 ),
c9a4f9ea 243 ],
9d73266c 244 ]);
89b7a234 245 this.chargingStation = chargingStation;
a37fc6dc
JB
246 this.onmessage = this.requestHandler.bind(this) as (message: unknown) => void;
247 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: unknown) => void;
89b7a234
JB
248 }
249
02a6943a 250 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
5dea4c94
JB
251 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
252 if (validatedMessageEvent === false) {
6c8f5d90
JB
253 return;
254 }
5dea4c94
JB
255 if (this.isResponse(validatedMessageEvent.data) === true) {
256 return;
257 }
258 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
2afb4d15 259 if (
4a3807d1
JB
260 !isNullOrUndefined(requestPayload.hashIds) &&
261 requestPayload.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
2afb4d15
JB
262 ) {
263 return;
264 }
4a3807d1 265 if (!isNullOrUndefined(requestPayload.hashId)) {
2afb4d15 266 logger.error(
5edd8ba0 267 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`,
2afb4d15
JB
268 );
269 return;
4eca248c 270 }
e1d9a0f4
JB
271 let responsePayload: BroadcastChannelResponsePayload | undefined;
272 let commandResponse: CommandResponse | void | undefined;
6c8f5d90
JB
273 try {
274 commandResponse = await this.commandHandler(command, requestPayload);
9bf0ef23 275 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
10d244c0 276 responsePayload = {
51c83d6f 277 hashId: this.chargingStation.stationInfo.hashId,
10d244c0
JB
278 status: ResponseStatus.SUCCESS,
279 };
6c8f5d90 280 } else {
d3195f0a 281 responsePayload = this.commandResponseToResponsePayload(
1984f194 282 command,
d3195f0a 283 requestPayload,
5edd8ba0 284 commandResponse as CommandResponse,
1984f194 285 );
6c8f5d90
JB
286 }
287 } catch (error) {
288 logger.error(
289 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
5edd8ba0 290 error,
6c8f5d90
JB
291 );
292 responsePayload = {
51c83d6f 293 hashId: this.chargingStation.stationInfo.hashId,
6c8f5d90
JB
294 status: ResponseStatus.FAILURE,
295 command,
296 requestPayload,
9d73266c 297 commandResponse: commandResponse as CommandResponse,
6c8f5d90
JB
298 errorMessage: (error as Error).message,
299 errorStack: (error as Error).stack,
a9ed42b2 300 errorDetails: (error as OCPPError).details,
6c8f5d90 301 };
623b39b5 302 } finally {
e1d9a0f4 303 this.sendResponse([uuid, responsePayload!]);
6c8f5d90 304 }
6c8f5d90
JB
305 }
306
307 private messageErrorHandler(messageEvent: MessageEvent): void {
308 logger.error(
309 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
5edd8ba0 310 messageEvent,
6c8f5d90
JB
311 );
312 }
313
314 private async commandHandler(
315 command: BroadcastChannelProcedureName,
5edd8ba0 316 requestPayload: BroadcastChannelRequestPayload,
9d73266c
JB
317 ): Promise<CommandResponse | void> {
318 if (this.commandHandlers.has(command) === true) {
1984f194 319 this.cleanRequestPayload(command, requestPayload);
e1d9a0f4 320 return this.commandHandlers.get(command)!(requestPayload);
6c8f5d90 321 }
90aceaf6 322 throw new BaseError(`Unknown worker broadcast channel command: '${command}'`);
6c8f5d90
JB
323 }
324
1984f194
JB
325 private cleanRequestPayload(
326 command: BroadcastChannelProcedureName,
5edd8ba0 327 requestPayload: BroadcastChannelRequestPayload,
1984f194
JB
328 ): void {
329 delete requestPayload.hashId;
330 delete requestPayload.hashIds;
331 [
332 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
333 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
8f879946 334 ].includes(command) === false && delete requestPayload.connectorIds;
1984f194
JB
335 }
336
d3195f0a
JB
337 private commandResponseToResponsePayload(
338 command: BroadcastChannelProcedureName,
339 requestPayload: BroadcastChannelRequestPayload,
5edd8ba0 340 commandResponse: CommandResponse,
d3195f0a 341 ): BroadcastChannelResponsePayload {
cfa257f5
JB
342 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
343 if (responseStatus === ResponseStatus.SUCCESS) {
d3195f0a
JB
344 return {
345 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 346 status: responseStatus,
d3195f0a
JB
347 };
348 }
349 return {
350 hashId: this.chargingStation.stationInfo.hashId,
cfa257f5 351 status: responseStatus,
d3195f0a
JB
352 command,
353 requestPayload,
354 commandResponse,
355 };
356 }
357
cfa257f5 358 private commandResponseToResponseStatus(
10db00b2 359 command: BroadcastChannelProcedureName,
5edd8ba0 360 commandResponse: CommandResponse,
10db00b2
JB
361 ): ResponseStatus {
362 switch (command) {
363 case BroadcastChannelProcedureName.START_TRANSACTION:
364 case BroadcastChannelProcedureName.STOP_TRANSACTION:
1984f194 365 case BroadcastChannelProcedureName.AUTHORIZE:
10db00b2 366 if (
1984f194
JB
367 (
368 commandResponse as
369 | StartTransactionResponse
370 | StopTransactionResponse
371 | AuthorizeResponse
372 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
10db00b2
JB
373 ) {
374 return ResponseStatus.SUCCESS;
375 }
376 return ResponseStatus.FAILURE;
8bfbc743 377 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
d270cc87 378 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
8bfbc743
JB
379 return ResponseStatus.SUCCESS;
380 }
381 return ResponseStatus.FAILURE;
91a7d3ea
JB
382 case BroadcastChannelProcedureName.DATA_TRANSFER:
383 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
384 return ResponseStatus.SUCCESS;
385 }
386 return ResponseStatus.FAILURE;
10db00b2 387 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
d3195f0a 388 case BroadcastChannelProcedureName.METER_VALUES:
9bf0ef23 389 if (isEmptyObject(commandResponse) === true) {
10db00b2
JB
390 return ResponseStatus.SUCCESS;
391 }
392 return ResponseStatus.FAILURE;
393 case BroadcastChannelProcedureName.HEARTBEAT:
394 if ('currentTime' in commandResponse) {
395 return ResponseStatus.SUCCESS;
396 }
397 return ResponseStatus.FAILURE;
398 default:
399 return ResponseStatus.FAILURE;
89b7a234
JB
400 }
401 }
402}