fix: ensure reservation expiration internal is > 0
[e-mobility-charging-stations-simulator.git] / src / charging-station / broadcast-channel / ChargingStationWorkerBroadcastChannel.ts
1 import { WorkerBroadcastChannel } from './WorkerBroadcastChannel';
2 import { BaseError, type OCPPError } from '../../exception';
3 import {
4 AuthorizationStatus,
5 type AuthorizeRequest,
6 type AuthorizeResponse,
7 type BootNotificationRequest,
8 type BootNotificationResponse,
9 BroadcastChannelProcedureName,
10 type BroadcastChannelRequest,
11 type BroadcastChannelRequestPayload,
12 type BroadcastChannelResponsePayload,
13 type DataTransferRequest,
14 type DataTransferResponse,
15 DataTransferStatus,
16 type DiagnosticsStatusNotificationRequest,
17 type DiagnosticsStatusNotificationResponse,
18 type FirmwareStatusNotificationRequest,
19 type FirmwareStatusNotificationResponse,
20 type HeartbeatRequest,
21 type HeartbeatResponse,
22 type MessageEvent,
23 type MeterValuesRequest,
24 type MeterValuesResponse,
25 RegistrationStatusEnumType,
26 RequestCommand,
27 type RequestParams,
28 ResponseStatus,
29 StandardParametersKey,
30 type StartTransactionRequest,
31 type StartTransactionResponse,
32 type StatusNotificationRequest,
33 type StatusNotificationResponse,
34 type StopTransactionRequest,
35 type StopTransactionResponse,
36 } from '../../types';
37 import { Constants, convertToInt, isEmptyObject, isNullOrUndefined, logger } from '../../utils';
38 import type { ChargingStation } from '../ChargingStation';
39 import { ChargingStationConfigurationUtils } from '../ChargingStationConfigurationUtils';
40 import { OCPP16ServiceUtils } from '../ocpp';
41
42 const moduleName = 'ChargingStationWorkerBroadcastChannel';
43
44 type CommandResponse =
45 | StartTransactionResponse
46 | StopTransactionResponse
47 | AuthorizeResponse
48 | BootNotificationResponse
49 | StatusNotificationResponse
50 | HeartbeatResponse
51 | DataTransferResponse;
52
53 type CommandHandler = (
54 requestPayload?: BroadcastChannelRequestPayload,
55 ) => Promise<CommandResponse | void> | void;
56
57 export class ChargingStationWorkerBroadcastChannel extends WorkerBroadcastChannel {
58 private readonly commandHandlers: Map<BroadcastChannelProcedureName, CommandHandler>;
59 private readonly chargingStation: ChargingStation;
60
61 constructor(chargingStation: ChargingStation) {
62 super();
63 const requestParams: RequestParams = {
64 throwError: true,
65 };
66 this.commandHandlers = new Map<BroadcastChannelProcedureName, CommandHandler>([
67 [BroadcastChannelProcedureName.START_CHARGING_STATION, () => this.chargingStation.start()],
68 [
69 BroadcastChannelProcedureName.STOP_CHARGING_STATION,
70 async () => this.chargingStation.stop(),
71 ],
72 [
73 BroadcastChannelProcedureName.OPEN_CONNECTION,
74 () => this.chargingStation.openWSConnection(),
75 ],
76 [
77 BroadcastChannelProcedureName.CLOSE_CONNECTION,
78 () => this.chargingStation.closeWSConnection(),
79 ],
80 [
81 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
82 (requestPayload?: BroadcastChannelRequestPayload) =>
83 this.chargingStation.startAutomaticTransactionGenerator(requestPayload?.connectorIds),
84 ],
85 [
86 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
87 (requestPayload?: BroadcastChannelRequestPayload) =>
88 this.chargingStation.stopAutomaticTransactionGenerator(requestPayload?.connectorIds),
89 ],
90 [
91 BroadcastChannelProcedureName.SET_SUPERVISION_URL,
92 (requestPayload?: BroadcastChannelRequestPayload) =>
93 this.chargingStation.setSupervisionUrl(requestPayload?.url as string),
94 ],
95 [
96 BroadcastChannelProcedureName.START_TRANSACTION,
97 async (requestPayload?: BroadcastChannelRequestPayload) =>
98 this.chargingStation.ocppRequestService.requestHandler<
99 StartTransactionRequest,
100 StartTransactionResponse
101 >(this.chargingStation, RequestCommand.START_TRANSACTION, requestPayload, requestParams),
102 ],
103 [
104 BroadcastChannelProcedureName.STOP_TRANSACTION,
105 async (requestPayload?: BroadcastChannelRequestPayload) =>
106 this.chargingStation.ocppRequestService.requestHandler<
107 StopTransactionRequest,
108 StartTransactionResponse
109 >(
110 this.chargingStation,
111 RequestCommand.STOP_TRANSACTION,
112 {
113 meterStop: this.chargingStation.getEnergyActiveImportRegisterByTransactionId(
114 requestPayload!.transactionId!,
115 true,
116 ),
117 ...requestPayload,
118 },
119 requestParams,
120 ),
121 ],
122 [
123 BroadcastChannelProcedureName.AUTHORIZE,
124 async (requestPayload?: BroadcastChannelRequestPayload) =>
125 this.chargingStation.ocppRequestService.requestHandler<
126 AuthorizeRequest,
127 AuthorizeResponse
128 >(this.chargingStation, RequestCommand.AUTHORIZE, requestPayload, requestParams),
129 ],
130 [
131 BroadcastChannelProcedureName.BOOT_NOTIFICATION,
132 async (requestPayload?: BroadcastChannelRequestPayload) => {
133 this.chargingStation.bootNotificationResponse =
134 await this.chargingStation.ocppRequestService.requestHandler<
135 BootNotificationRequest,
136 BootNotificationResponse
137 >(
138 this.chargingStation,
139 RequestCommand.BOOT_NOTIFICATION,
140 {
141 ...this.chargingStation.bootNotificationRequest,
142 ...requestPayload,
143 },
144 {
145 skipBufferingOnError: true,
146 throwError: true,
147 },
148 );
149 return this.chargingStation.bootNotificationResponse;
150 },
151 ],
152 [
153 BroadcastChannelProcedureName.STATUS_NOTIFICATION,
154 async (requestPayload?: BroadcastChannelRequestPayload) =>
155 this.chargingStation.ocppRequestService.requestHandler<
156 StatusNotificationRequest,
157 StatusNotificationResponse
158 >(
159 this.chargingStation,
160 RequestCommand.STATUS_NOTIFICATION,
161 requestPayload,
162 requestParams,
163 ),
164 ],
165 [
166 BroadcastChannelProcedureName.HEARTBEAT,
167 async (requestPayload?: BroadcastChannelRequestPayload) =>
168 this.chargingStation.ocppRequestService.requestHandler<
169 HeartbeatRequest,
170 HeartbeatResponse
171 >(this.chargingStation, RequestCommand.HEARTBEAT, requestPayload, requestParams),
172 ],
173 [
174 BroadcastChannelProcedureName.METER_VALUES,
175 async (requestPayload?: BroadcastChannelRequestPayload) => {
176 const configuredMeterValueSampleInterval =
177 ChargingStationConfigurationUtils.getConfigurationKey(
178 chargingStation,
179 StandardParametersKey.MeterValueSampleInterval,
180 );
181 return this.chargingStation.ocppRequestService.requestHandler<
182 MeterValuesRequest,
183 MeterValuesResponse
184 >(
185 this.chargingStation,
186 RequestCommand.METER_VALUES,
187 {
188 meterValue: [
189 // FIXME: Implement OCPP version agnostic helpers
190 OCPP16ServiceUtils.buildMeterValue(
191 this.chargingStation,
192 requestPayload!.connectorId!,
193 this.chargingStation.getConnectorStatus(requestPayload!.connectorId!)!
194 .transactionId!,
195 configuredMeterValueSampleInterval
196 ? convertToInt(configuredMeterValueSampleInterval.value) * 1000
197 : Constants.DEFAULT_METER_VALUES_INTERVAL,
198 ),
199 ],
200 ...requestPayload,
201 },
202 requestParams,
203 );
204 },
205 ],
206 [
207 BroadcastChannelProcedureName.DATA_TRANSFER,
208 async (requestPayload?: BroadcastChannelRequestPayload) =>
209 this.chargingStation.ocppRequestService.requestHandler<
210 DataTransferRequest,
211 DataTransferResponse
212 >(this.chargingStation, RequestCommand.DATA_TRANSFER, requestPayload, requestParams),
213 ],
214 [
215 BroadcastChannelProcedureName.DIAGNOSTICS_STATUS_NOTIFICATION,
216 async (requestPayload?: BroadcastChannelRequestPayload) =>
217 this.chargingStation.ocppRequestService.requestHandler<
218 DiagnosticsStatusNotificationRequest,
219 DiagnosticsStatusNotificationResponse
220 >(
221 this.chargingStation,
222 RequestCommand.DIAGNOSTICS_STATUS_NOTIFICATION,
223 requestPayload,
224 requestParams,
225 ),
226 ],
227 [
228 BroadcastChannelProcedureName.FIRMWARE_STATUS_NOTIFICATION,
229 async (requestPayload?: BroadcastChannelRequestPayload) =>
230 this.chargingStation.ocppRequestService.requestHandler<
231 FirmwareStatusNotificationRequest,
232 FirmwareStatusNotificationResponse
233 >(
234 this.chargingStation,
235 RequestCommand.FIRMWARE_STATUS_NOTIFICATION,
236 requestPayload,
237 requestParams,
238 ),
239 ],
240 ]);
241 this.chargingStation = chargingStation;
242 this.onmessage = this.requestHandler.bind(this) as (message: MessageEvent) => void;
243 this.onmessageerror = this.messageErrorHandler.bind(this) as (message: MessageEvent) => void;
244 }
245
246 private async requestHandler(messageEvent: MessageEvent): Promise<void> {
247 const validatedMessageEvent = this.validateMessageEvent(messageEvent);
248 if (validatedMessageEvent === false) {
249 return;
250 }
251 if (this.isResponse(validatedMessageEvent.data) === true) {
252 return;
253 }
254 const [uuid, command, requestPayload] = validatedMessageEvent.data as BroadcastChannelRequest;
255 if (
256 !isNullOrUndefined(requestPayload?.hashIds) &&
257 requestPayload?.hashIds?.includes(this.chargingStation.stationInfo.hashId) === false
258 ) {
259 return;
260 }
261 if (!isNullOrUndefined(requestPayload?.hashId)) {
262 logger.error(
263 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: 'hashId' field usage in PDU is deprecated, use 'hashIds' array instead`,
264 );
265 return;
266 }
267 let responsePayload: BroadcastChannelResponsePayload | undefined;
268 let commandResponse: CommandResponse | void | undefined;
269 try {
270 commandResponse = await this.commandHandler(command, requestPayload);
271 if (isNullOrUndefined(commandResponse) || isEmptyObject(commandResponse as CommandResponse)) {
272 responsePayload = {
273 hashId: this.chargingStation.stationInfo.hashId,
274 status: ResponseStatus.SUCCESS,
275 };
276 } else {
277 responsePayload = this.commandResponseToResponsePayload(
278 command,
279 requestPayload,
280 commandResponse as CommandResponse,
281 );
282 }
283 } catch (error) {
284 logger.error(
285 `${this.chargingStation.logPrefix()} ${moduleName}.requestHandler: Handle request error:`,
286 error,
287 );
288 responsePayload = {
289 hashId: this.chargingStation.stationInfo.hashId,
290 status: ResponseStatus.FAILURE,
291 command,
292 requestPayload,
293 commandResponse: commandResponse as CommandResponse,
294 errorMessage: (error as Error).message,
295 errorStack: (error as Error).stack,
296 errorDetails: (error as OCPPError).details,
297 };
298 } finally {
299 this.sendResponse([uuid, responsePayload!]);
300 }
301 }
302
303 private messageErrorHandler(messageEvent: MessageEvent): void {
304 logger.error(
305 `${this.chargingStation.logPrefix()} ${moduleName}.messageErrorHandler: Error at handling message:`,
306 messageEvent,
307 );
308 }
309
310 private async commandHandler(
311 command: BroadcastChannelProcedureName,
312 requestPayload: BroadcastChannelRequestPayload,
313 ): Promise<CommandResponse | void> {
314 if (this.commandHandlers.has(command) === true) {
315 this.cleanRequestPayload(command, requestPayload);
316 return this.commandHandlers.get(command)!(requestPayload);
317 }
318 throw new BaseError(`Unknown worker broadcast channel command: ${command}`);
319 }
320
321 private cleanRequestPayload(
322 command: BroadcastChannelProcedureName,
323 requestPayload: BroadcastChannelRequestPayload,
324 ): void {
325 delete requestPayload.hashId;
326 delete requestPayload.hashIds;
327 [
328 BroadcastChannelProcedureName.START_AUTOMATIC_TRANSACTION_GENERATOR,
329 BroadcastChannelProcedureName.STOP_AUTOMATIC_TRANSACTION_GENERATOR,
330 ].includes(command) === false && delete requestPayload.connectorIds;
331 }
332
333 private commandResponseToResponsePayload(
334 command: BroadcastChannelProcedureName,
335 requestPayload: BroadcastChannelRequestPayload,
336 commandResponse: CommandResponse,
337 ): BroadcastChannelResponsePayload {
338 const responseStatus = this.commandResponseToResponseStatus(command, commandResponse);
339 if (responseStatus === ResponseStatus.SUCCESS) {
340 return {
341 hashId: this.chargingStation.stationInfo.hashId,
342 status: responseStatus,
343 };
344 }
345 return {
346 hashId: this.chargingStation.stationInfo.hashId,
347 status: responseStatus,
348 command,
349 requestPayload,
350 commandResponse,
351 };
352 }
353
354 private commandResponseToResponseStatus(
355 command: BroadcastChannelProcedureName,
356 commandResponse: CommandResponse,
357 ): ResponseStatus {
358 switch (command) {
359 case BroadcastChannelProcedureName.START_TRANSACTION:
360 case BroadcastChannelProcedureName.STOP_TRANSACTION:
361 case BroadcastChannelProcedureName.AUTHORIZE:
362 if (
363 (
364 commandResponse as
365 | StartTransactionResponse
366 | StopTransactionResponse
367 | AuthorizeResponse
368 )?.idTagInfo?.status === AuthorizationStatus.ACCEPTED
369 ) {
370 return ResponseStatus.SUCCESS;
371 }
372 return ResponseStatus.FAILURE;
373 case BroadcastChannelProcedureName.BOOT_NOTIFICATION:
374 if (commandResponse?.status === RegistrationStatusEnumType.ACCEPTED) {
375 return ResponseStatus.SUCCESS;
376 }
377 return ResponseStatus.FAILURE;
378 case BroadcastChannelProcedureName.DATA_TRANSFER:
379 if (commandResponse?.status === DataTransferStatus.ACCEPTED) {
380 return ResponseStatus.SUCCESS;
381 }
382 return ResponseStatus.FAILURE;
383 case BroadcastChannelProcedureName.STATUS_NOTIFICATION:
384 case BroadcastChannelProcedureName.METER_VALUES:
385 if (isEmptyObject(commandResponse) === true) {
386 return ResponseStatus.SUCCESS;
387 }
388 return ResponseStatus.FAILURE;
389 case BroadcastChannelProcedureName.HEARTBEAT:
390 if ('currentTime' in commandResponse) {
391 return ResponseStatus.SUCCESS;
392 }
393 return ResponseStatus.FAILURE;
394 default:
395 return ResponseStatus.FAILURE;
396 }
397 }
398 }