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