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