refactor(simulator): switch utils to internal module export/import
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
edd13439 1// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
b4d34251 2
43ee4373 3import crypto from 'node:crypto';
130783a7
JB
4import fs from 'node:fs';
5import path from 'node:path';
6import { URL } from 'node:url';
8114d10e
JB
7import { parentPort } from 'worker_threads';
8
5d280aae 9import merge from 'just-merge';
ef7d8c21 10import WebSocket, { type RawData } from 'ws';
8114d10e 11
368a6eda 12import {
2896e06d
JB
13 AuthorizedTagsCache,
14 AutomaticTransactionGenerator,
15 ChargingStationConfigurationUtils,
16 ChargingStationUtils,
17 ChargingStationWorkerBroadcastChannel,
18 MessageChannelUtils,
19 SharedLRUCache,
20} from './internal';
21import {
22 // OCPP16IncomingRequestService,
368a6eda 23 OCPP16RequestService,
2896e06d 24 // OCPP16ResponseService,
368a6eda
JB
25 OCPP16ServiceUtils,
26 OCPP20IncomingRequestService,
27 OCPP20RequestService,
2896e06d 28 // OCPP20ResponseService,
368a6eda
JB
29 type OCPPIncomingRequestService,
30 type OCPPRequestService,
2896e06d 31 // OCPPServiceUtils,
368a6eda 32} from './ocpp';
2896e06d
JB
33import { OCPP16IncomingRequestService } from './ocpp/1.6/OCPP16IncomingRequestService';
34import { OCPP16ResponseService } from './ocpp/1.6/OCPP16ResponseService';
35import { OCPP20ResponseService } from './ocpp/2.0/OCPP20ResponseService';
36import { OCPPServiceUtils } from './ocpp/OCPPServiceUtils';
268a74bb 37import { BaseError, OCPPError } from '../exception';
2896e06d
JB
38// import { PerformanceStatistics } from '../performance';
39import { PerformanceStatistics } from '../performance/PerformanceStatistics';
e7aeea18 40import {
268a74bb 41 type AutomaticTransactionGeneratorConfiguration,
e7aeea18 42 AvailabilityType,
e0b0ee21 43 type BootNotificationRequest,
268a74bb 44 type BootNotificationResponse,
e0b0ee21 45 type CachedRequest,
268a74bb
JB
46 type ChargingStationConfiguration,
47 type ChargingStationInfo,
48 type ChargingStationOcppConfiguration,
49 type ChargingStationTemplate,
50 ConnectorPhaseRotation,
51 ConnectorStatus,
52 ConnectorStatusEnum,
53 CurrentType,
e0b0ee21 54 type ErrorCallback,
268a74bb
JB
55 type ErrorResponse,
56 ErrorType,
57 FileType,
c9a4f9ea
JB
58 FirmwareStatus,
59 type FirmwareStatusNotificationRequest,
268a74bb
JB
60 type FirmwareStatusNotificationResponse,
61 type FirmwareUpgrade,
e0b0ee21 62 type HeartbeatRequest,
268a74bb 63 type HeartbeatResponse,
e0b0ee21 64 type IncomingRequest,
268a74bb
JB
65 type IncomingRequestCommand,
66 type JsonType,
67 MessageType,
68 type MeterValue,
69 MeterValueMeasurand,
e0b0ee21 70 type MeterValuesRequest,
268a74bb
JB
71 type MeterValuesResponse,
72 OCPPVersion,
8ca6874c 73 type OutgoingRequest,
268a74bb
JB
74 PowerUnits,
75 RegistrationStatusEnumType,
e7aeea18 76 RequestCommand,
268a74bb 77 type Response,
e0b0ee21 78 type ResponseCallback,
268a74bb 79 StandardParametersKey,
e0b0ee21 80 type StatusNotificationRequest,
e0b0ee21 81 type StatusNotificationResponse,
ef6fa3fb 82 StopTransactionReason,
e0b0ee21
JB
83 type StopTransactionRequest,
84 type StopTransactionResponse,
268a74bb
JB
85 SupervisionUrlDistribution,
86 SupportedFeatureProfiles,
87 VendorDefaultParametersKey,
88 type WSError,
89 WebSocketCloseEventStatusCode,
90 type WsOptions,
91} from '../types';
60a74391
JB
92import {
93 ACElectricUtils,
94 Configuration,
95 Constants,
96 DCElectricUtils,
97 FileUtils,
98 Utils,
99 logger,
100} from '../utils';
3f40bc9c 101
268a74bb 102export class ChargingStation {
c72f6634 103 public readonly index: number;
2484ac1e 104 public readonly templateFile: string;
6e0964c8 105 public stationInfo!: ChargingStationInfo;
452a82ca 106 public started: boolean;
cbf9b878 107 public starting: boolean;
a5e9befc 108 public authorizedTagsCache: AuthorizedTagsCache;
551e477c
JB
109 public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
110 public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
72092cfc 111 public wsConnection!: WebSocket | null;
a5e9befc 112 public readonly connectors: Map<number, ConnectorStatus>;
9e23580d 113 public readonly requests: Map<string, CachedRequest>;
551e477c 114 public performanceStatistics!: PerformanceStatistics | undefined;
6e0964c8 115 public heartbeatSetInterval!: NodeJS.Timeout;
6e0964c8 116 public ocppRequestService!: OCPPRequestService;
0a03f36c 117 public bootNotificationRequest!: BootNotificationRequest;
1895299d 118 public bootNotificationResponse!: BootNotificationResponse | undefined;
fa7bccf4 119 public powerDivider!: number;
950b1349 120 private stopping: boolean;
073bd098 121 private configurationFile!: string;
7c72977b 122 private configurationFileHash!: string;
6e0964c8 123 private connectorsConfigurationHash!: string;
a472cf2b 124 private ocppIncomingRequestService!: OCPPIncomingRequestService;
8e242273 125 private readonly messageBuffer: Set<string>;
fa7bccf4 126 private configuredSupervisionUrl!: URL;
c72f6634 127 private configuredSupervisionUrlIndex!: number;
265e4266 128 private wsConnectionRestarted: boolean;
ad2f27c3 129 private autoReconnectRetryCount: number;
72092cfc 130 private templateFileWatcher!: fs.FSWatcher | undefined;
57adbebc 131 private readonly sharedLRUCache: SharedLRUCache;
6e0964c8 132 private webSocketPingSetInterval!: NodeJS.Timeout;
89b7a234 133 private readonly chargingStationWorkerBroadcastChannel: ChargingStationWorkerBroadcastChannel;
6af9012e 134
2484ac1e 135 constructor(index: number, templateFile: string) {
aa428a31 136 this.started = false;
950b1349
JB
137 this.starting = false;
138 this.stopping = false;
aa428a31
JB
139 this.wsConnectionRestarted = false;
140 this.autoReconnectRetryCount = 0;
ad2f27c3 141 this.index = index;
2484ac1e 142 this.templateFile = templateFile;
9f2e3130 143 this.connectors = new Map<number, ConnectorStatus>();
32b02249 144 this.requests = new Map<string, CachedRequest>();
8e242273 145 this.messageBuffer = new Set<string>();
b44b779a
JB
146 this.sharedLRUCache = SharedLRUCache.getInstance();
147 this.authorizedTagsCache = AuthorizedTagsCache.getInstance();
89b7a234 148 this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
32de5a57 149
9f2e3130 150 this.initialize();
c0560973
JB
151 }
152
25f5a959 153 private get wsConnectionUrl(): URL {
fa7bccf4 154 return new URL(
44eb6026
JB
155 `${
156 this.getSupervisionUrlOcppConfiguration()
157 ? ChargingStationConfigurationUtils.getConfigurationKey(
158 this,
159 this.getSupervisionUrlOcppKey()
72092cfc 160 )?.value
44eb6026
JB
161 : this.configuredSupervisionUrl.href
162 }/${this.stationInfo.chargingStationId}`
fa7bccf4 163 );
12fc74d6
JB
164 }
165
8b7072dc 166 public logPrefix = (): string => {
ccb1d6e9
JB
167 return Utils.logPrefix(
168 ` ${
5a2a53cf 169 (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId) &&
1b271a54
JB
170 this?.stationInfo?.chargingStationId) ??
171 ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile()) ??
172 ''
ccb1d6e9
JB
173 } |`
174 );
8b7072dc 175 };
c0560973 176
c0560973 177 public hasAuthorizedTags(): boolean {
53ac516c 178 return Utils.isNotEmptyArray(
9d7484a4
JB
179 this.authorizedTagsCache.getAuthorizedTags(
180 ChargingStationUtils.getAuthorizationFile(this.stationInfo)
181 )
182 );
c0560973
JB
183 }
184
ad774cec
JB
185 public getEnableStatistics(): boolean {
186 return this.stationInfo.enableStatistics ?? false;
c0560973
JB
187 }
188
ad774cec 189 public getMustAuthorizeAtRemoteStart(): boolean {
03ebf4c1 190 return this.stationInfo.mustAuthorizeAtRemoteStart ?? true;
a7fc8211
JB
191 }
192
ad774cec 193 public getPayloadSchemaValidation(): boolean {
e3018bc4
JB
194 return this.stationInfo.payloadSchemaValidation ?? true;
195 }
196
fa7bccf4
JB
197 public getNumberOfPhases(stationInfo?: ChargingStationInfo): number | undefined {
198 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
199 switch (this.getCurrentOutType(stationInfo)) {
4c2b4904 200 case CurrentType.AC:
fa7bccf4
JB
201 return !Utils.isUndefined(localStationInfo.numberOfPhases)
202 ? localStationInfo.numberOfPhases
e7aeea18 203 : 3;
4c2b4904 204 case CurrentType.DC:
c0560973
JB
205 return 0;
206 }
207 }
208
d5bff457 209 public isWebSocketConnectionOpened(): boolean {
0d8140bd 210 return this?.wsConnection?.readyState === WebSocket.OPEN;
c0560973
JB
211 }
212
1895299d 213 public getRegistrationStatus(): RegistrationStatusEnumType | undefined {
672fed6e
JB
214 return this?.bootNotificationResponse?.status;
215 }
216
73c4266d
JB
217 public isInUnknownState(): boolean {
218 return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
219 }
220
16cd35ad 221 public isInPendingState(): boolean {
d270cc87 222 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.PENDING;
16cd35ad
JB
223 }
224
225 public isInAcceptedState(): boolean {
d270cc87 226 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.ACCEPTED;
c0560973
JB
227 }
228
16cd35ad 229 public isInRejectedState(): boolean {
d270cc87 230 return this?.bootNotificationResponse?.status === RegistrationStatusEnumType.REJECTED;
16cd35ad
JB
231 }
232
233 public isRegistered(): boolean {
ed6cfcff
JB
234 return (
235 this.isInUnknownState() === false &&
236 (this.isInAcceptedState() === true || this.isInPendingState() === true)
237 );
16cd35ad
JB
238 }
239
c0560973 240 public isChargingStationAvailable(): boolean {
1895299d 241 return this.getConnectorStatus(0)?.availability === AvailabilityType.OPERATIVE;
c0560973
JB
242 }
243
244 public isConnectorAvailable(id: number): boolean {
1895299d 245 return id > 0 && this.getConnectorStatus(id)?.availability === AvailabilityType.OPERATIVE;
c0560973
JB
246 }
247
54544ef1
JB
248 public getNumberOfConnectors(): number {
249 return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
250 }
251
6d9876e7 252 public getConnectorStatus(id: number): ConnectorStatus | undefined {
734d790d 253 return this.connectors.get(id);
c0560973
JB
254 }
255
fa7bccf4 256 public getCurrentOutType(stationInfo?: ChargingStationInfo): CurrentType {
72092cfc 257 return (stationInfo ?? this.stationInfo)?.currentOutType ?? CurrentType.AC;
c0560973
JB
258 }
259
672fed6e 260 public getOcppStrictCompliance(): boolean {
ccb1d6e9 261 return this.stationInfo?.ocppStrictCompliance ?? false;
672fed6e
JB
262 }
263
fa7bccf4 264 public getVoltageOut(stationInfo?: ChargingStationInfo): number | undefined {
492cf6ab 265 const defaultVoltageOut = ChargingStationUtils.getDefaultVoltageOut(
fa7bccf4 266 this.getCurrentOutType(stationInfo),
492cf6ab
JB
267 this.templateFile,
268 this.logPrefix()
269 );
fa7bccf4
JB
270 const localStationInfo: ChargingStationInfo = stationInfo ?? this.stationInfo;
271 return !Utils.isUndefined(localStationInfo.voltageOut)
272 ? localStationInfo.voltageOut
e7aeea18 273 : defaultVoltageOut;
c0560973
JB
274 }
275
15068be9
JB
276 public getMaximumPower(stationInfo?: ChargingStationInfo): number {
277 const localStationInfo = stationInfo ?? this.stationInfo;
278 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
279 }
280
ad8537a7 281 public getConnectorMaximumAvailablePower(connectorId: number): number {
d20f43b5 282 let connectorAmperageLimitationPowerLimit: number;
b47d68d7
JB
283 if (
284 !Utils.isNullOrUndefined(this.getAmperageLimitation()) &&
72092cfc 285 this.getAmperageLimitation() < this.stationInfo?.maximumAmperage
b47d68d7 286 ) {
4160ae28
JB
287 connectorAmperageLimitationPowerLimit =
288 (this.getCurrentOutType() === CurrentType.AC
cc6e8ab5
JB
289 ? ACElectricUtils.powerTotal(
290 this.getNumberOfPhases(),
291 this.getVoltageOut(),
da57964c 292 this.getAmperageLimitation() * this.getNumberOfConnectors()
cc6e8ab5 293 )
4160ae28 294 : DCElectricUtils.power(this.getVoltageOut(), this.getAmperageLimitation())) /
fa7bccf4 295 this.powerDivider;
cc6e8ab5 296 }
fa7bccf4 297 const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
15068be9
JB
298 const connectorChargingProfilesPowerLimit =
299 ChargingStationUtils.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
ad8537a7
JB
300 return Math.min(
301 isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
302 isNaN(connectorAmperageLimitationPowerLimit)
303 ? Infinity
304 : connectorAmperageLimitationPowerLimit,
15068be9 305 isNaN(connectorChargingProfilesPowerLimit) ? Infinity : connectorChargingProfilesPowerLimit
ad8537a7 306 );
cc6e8ab5
JB
307 }
308
6e0964c8 309 public getTransactionIdTag(transactionId: number): string | undefined {
734d790d 310 for (const connectorId of this.connectors.keys()) {
1895299d
JB
311 if (
312 connectorId > 0 &&
313 this.getConnectorStatus(connectorId)?.transactionId === transactionId
314 ) {
72092cfc 315 return this.getConnectorStatus(connectorId)?.transactionIdTag;
c0560973
JB
316 }
317 }
318 }
319
6ed92bc1 320 public getOutOfOrderEndMeterValues(): boolean {
ccb1d6e9 321 return this.stationInfo?.outOfOrderEndMeterValues ?? false;
6ed92bc1
JB
322 }
323
324 public getBeginEndMeterValues(): boolean {
ccb1d6e9 325 return this.stationInfo?.beginEndMeterValues ?? false;
6ed92bc1
JB
326 }
327
328 public getMeteringPerTransaction(): boolean {
ccb1d6e9 329 return this.stationInfo?.meteringPerTransaction ?? true;
6ed92bc1
JB
330 }
331
fd0c36fa 332 public getTransactionDataMeterValues(): boolean {
ccb1d6e9 333 return this.stationInfo?.transactionDataMeterValues ?? false;
fd0c36fa
JB
334 }
335
9ccca265 336 public getMainVoltageMeterValues(): boolean {
ccb1d6e9 337 return this.stationInfo?.mainVoltageMeterValues ?? true;
9ccca265
JB
338 }
339
6b10669b 340 public getPhaseLineToLineVoltageMeterValues(): boolean {
ccb1d6e9 341 return this.stationInfo?.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
342 }
343
7bc31f9c 344 public getCustomValueLimitationMeterValues(): boolean {
ccb1d6e9 345 return this.stationInfo?.customValueLimitationMeterValues ?? true;
7bc31f9c
JB
346 }
347
f479a792 348 public getConnectorIdByTransactionId(transactionId: number): number | undefined {
734d790d 349 for (const connectorId of this.connectors.keys()) {
f479a792
JB
350 if (
351 connectorId > 0 &&
352 this.getConnectorStatus(connectorId)?.transactionId === transactionId
353 ) {
354 return connectorId;
c0560973
JB
355 }
356 }
357 }
358
07989fad
JB
359 public getEnergyActiveImportRegisterByTransactionId(
360 transactionId: number,
18bf8274 361 rounded = false
07989fad
JB
362 ): number {
363 return this.getEnergyActiveImportRegister(
364 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId)),
18bf8274 365 rounded
cbad1217 366 );
cbad1217
JB
367 }
368
18bf8274
JB
369 public getEnergyActiveImportRegisterByConnectorId(connectorId: number, rounded = false): number {
370 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId), rounded);
6ed92bc1
JB
371 }
372
c0560973 373 public getAuthorizeRemoteTxRequests(): boolean {
17ac262c
JB
374 const authorizeRemoteTxRequests = ChargingStationConfigurationUtils.getConfigurationKey(
375 this,
e7aeea18
JB
376 StandardParametersKey.AuthorizeRemoteTxRequests
377 );
378 return authorizeRemoteTxRequests
379 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
380 : false;
c0560973
JB
381 }
382
383 public getLocalAuthListEnabled(): boolean {
17ac262c
JB
384 const localAuthListEnabled = ChargingStationConfigurationUtils.getConfigurationKey(
385 this,
e7aeea18
JB
386 StandardParametersKey.LocalAuthListEnabled
387 );
c0560973
JB
388 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
389 }
390
c0560973 391 public startHeartbeat(): void {
e7aeea18
JB
392 if (
393 this.getHeartbeatInterval() &&
394 this.getHeartbeatInterval() > 0 &&
395 !this.heartbeatSetInterval
396 ) {
6a8329b4
JB
397 this.heartbeatSetInterval = setInterval(() => {
398 this.ocppRequestService
399 .requestHandler<HeartbeatRequest, HeartbeatResponse>(this, RequestCommand.HEARTBEAT)
72092cfc 400 .catch((error) => {
6a8329b4
JB
401 logger.error(
402 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
403 error
404 );
405 });
c0560973 406 }, this.getHeartbeatInterval());
e7aeea18 407 logger.info(
44eb6026
JB
408 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
409 this.getHeartbeatInterval()
410 )}`
e7aeea18 411 );
c0560973 412 } else if (this.heartbeatSetInterval) {
e7aeea18 413 logger.info(
44eb6026
JB
414 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
415 this.getHeartbeatInterval()
416 )}`
e7aeea18 417 );
c0560973 418 } else {
e7aeea18
JB
419 logger.error(
420 `${this.logPrefix()} Heartbeat interval set to ${
421 this.getHeartbeatInterval()
422 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
423 : this.getHeartbeatInterval()
424 }, not starting the heartbeat`
425 );
c0560973
JB
426 }
427 }
428
429 public restartHeartbeat(): void {
430 // Stop heartbeat
431 this.stopHeartbeat();
432 // Start heartbeat
433 this.startHeartbeat();
434 }
435
17ac262c
JB
436 public restartWebSocketPing(): void {
437 // Stop WebSocket ping
438 this.stopWebSocketPing();
439 // Start WebSocket ping
440 this.startWebSocketPing();
441 }
442
c0560973
JB
443 public startMeterValues(connectorId: number, interval: number): void {
444 if (connectorId === 0) {
e7aeea18
JB
445 logger.error(
446 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
447 );
c0560973
JB
448 return;
449 }
734d790d 450 if (!this.getConnectorStatus(connectorId)) {
e7aeea18
JB
451 logger.error(
452 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
453 );
c0560973
JB
454 return;
455 }
5e3cb728 456 if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
e7aeea18
JB
457 logger.error(
458 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
459 );
c0560973 460 return;
e7aeea18 461 } else if (
5e3cb728 462 this.getConnectorStatus(connectorId)?.transactionStarted === true &&
d812bdcb 463 Utils.isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
e7aeea18
JB
464 ) {
465 logger.error(
466 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
467 );
c0560973
JB
468 return;
469 }
470 if (interval > 0) {
6a8329b4
JB
471 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(() => {
472 // FIXME: Implement OCPP version agnostic helpers
473 const meterValue: MeterValue = OCPP16ServiceUtils.buildMeterValue(
474 this,
475 connectorId,
476 this.getConnectorStatus(connectorId).transactionId,
477 interval
478 );
479 this.ocppRequestService
480 .requestHandler<MeterValuesRequest, MeterValuesResponse>(
08f130a0 481 this,
f22266fd
JB
482 RequestCommand.METER_VALUES,
483 {
484 connectorId,
72092cfc 485 transactionId: this.getConnectorStatus(connectorId)?.transactionId,
f22266fd
JB
486 meterValue: [meterValue],
487 }
6a8329b4 488 )
72092cfc 489 .catch((error) => {
6a8329b4
JB
490 logger.error(
491 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
492 error
493 );
494 });
495 }, interval);
c0560973 496 } else {
e7aeea18
JB
497 logger.error(
498 `${this.logPrefix()} Charging station ${
499 StandardParametersKey.MeterValueSampleInterval
500 } configuration set to ${
501 interval ? Utils.formatDurationMilliSeconds(interval) : interval
502 }, not sending MeterValues`
503 );
c0560973
JB
504 }
505 }
506
507 public start(): void {
0d8852a5
JB
508 if (this.started === false) {
509 if (this.starting === false) {
510 this.starting = true;
ad774cec 511 if (this.getEnableStatistics() === true) {
551e477c 512 this.performanceStatistics?.start();
0d8852a5
JB
513 }
514 this.openWSConnection();
515 // Monitor charging station template file
516 this.templateFileWatcher = FileUtils.watchJsonFile(
0d8852a5 517 this.templateFile,
7164966d
JB
518 FileType.ChargingStationTemplate,
519 this.logPrefix(),
520 undefined,
0d8852a5 521 (event, filename): void => {
5a2a53cf 522 if (Utils.isNotEmptyString(filename) && event === 'change') {
0d8852a5
JB
523 try {
524 logger.debug(
525 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
526 this.templateFile
527 } file have changed, reload`
528 );
529 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
530 // Initialize
531 this.initialize();
532 // Restart the ATG
533 this.stopAutomaticTransactionGenerator();
534 if (
535 this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true
536 ) {
537 this.startAutomaticTransactionGenerator();
538 }
ad774cec 539 if (this.getEnableStatistics() === true) {
551e477c 540 this.performanceStatistics?.restart();
0d8852a5 541 } else {
551e477c 542 this.performanceStatistics?.stop();
0d8852a5
JB
543 }
544 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
545 } catch (error) {
546 logger.error(
547 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
548 error
549 );
950b1349 550 }
a95873d8 551 }
a95873d8 552 }
0d8852a5 553 );
56eb297e 554 this.started = true;
1895299d 555 parentPort?.postMessage(MessageChannelUtils.buildStartedMessage(this));
0d8852a5
JB
556 this.starting = false;
557 } else {
558 logger.warn(`${this.logPrefix()} Charging station is already starting...`);
559 }
950b1349 560 } else {
0d8852a5 561 logger.warn(`${this.logPrefix()} Charging station is already started...`);
950b1349 562 }
c0560973
JB
563 }
564
60ddad53 565 public async stop(reason?: StopTransactionReason): Promise<void> {
0d8852a5
JB
566 if (this.started === true) {
567 if (this.stopping === false) {
568 this.stopping = true;
569 await this.stopMessageSequence(reason);
0d8852a5 570 this.closeWSConnection();
ad774cec 571 if (this.getEnableStatistics() === true) {
551e477c 572 this.performanceStatistics?.stop();
0d8852a5
JB
573 }
574 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
72092cfc 575 this.templateFileWatcher?.close();
0d8852a5 576 this.sharedLRUCache.deleteChargingStationTemplate(this.stationInfo?.templateHash);
1895299d 577 this.bootNotificationResponse = undefined;
0d8852a5 578 this.started = false;
1895299d 579 parentPort?.postMessage(MessageChannelUtils.buildStoppedMessage(this));
0d8852a5
JB
580 this.stopping = false;
581 } else {
582 logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
c0560973 583 }
950b1349 584 } else {
0d8852a5 585 logger.warn(`${this.logPrefix()} Charging station is already stopped...`);
c0560973 586 }
c0560973
JB
587 }
588
60ddad53
JB
589 public async reset(reason?: StopTransactionReason): Promise<void> {
590 await this.stop(reason);
94ec7e96 591 await Utils.sleep(this.stationInfo.resetTime);
fa7bccf4 592 this.initialize();
94ec7e96
JB
593 this.start();
594 }
595
17ac262c
JB
596 public saveOcppConfiguration(): void {
597 if (this.getOcppPersistentConfiguration()) {
7c72977b 598 this.saveConfiguration();
e6895390
JB
599 }
600 }
601
a2653482
JB
602 public resetConnectorStatus(connectorId: number): void {
603 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
604 this.getConnectorStatus(connectorId).idTagAuthorized = false;
605 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d 606 this.getConnectorStatus(connectorId).transactionStarted = false;
d812bdcb
JB
607 delete this.getConnectorStatus(connectorId)?.localAuthorizeIdTag;
608 delete this.getConnectorStatus(connectorId)?.authorizeIdTag;
609 delete this.getConnectorStatus(connectorId)?.transactionId;
610 delete this.getConnectorStatus(connectorId)?.transactionIdTag;
734d790d 611 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
d812bdcb 612 delete this.getConnectorStatus(connectorId)?.transactionBeginMeterValue;
dd119a6b 613 this.stopMeterValues(connectorId);
1895299d 614 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
615 }
616
72092cfc 617 public hasFeatureProfile(featureProfile: SupportedFeatureProfiles): boolean | undefined {
17ac262c
JB
618 return ChargingStationConfigurationUtils.getConfigurationKey(
619 this,
620 StandardParametersKey.SupportedFeatureProfiles
72092cfc 621 )?.value?.includes(featureProfile);
68cb8b91
JB
622 }
623
8e242273
JB
624 public bufferMessage(message: string): void {
625 this.messageBuffer.add(message);
3ba2381e
JB
626 }
627
db2336d9
JB
628 public openWSConnection(
629 options: WsOptions = this.stationInfo?.wsOptions ?? {},
630 params: { closeOpened?: boolean; terminateOpened?: boolean } = {
631 closeOpened: false,
632 terminateOpened: false,
633 }
634 ): void {
635 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
636 params.closeOpened = params?.closeOpened ?? false;
637 params.terminateOpened = params?.terminateOpened ?? false;
cbf9b878 638 if (this.started === false && this.starting === false) {
d1c6c833
JB
639 logger.warn(
640 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()} on stopped charging station`
641 );
642 return;
643 }
db2336d9
JB
644 if (
645 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
646 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
647 ) {
648 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
649 }
650 if (params?.closeOpened) {
651 this.closeWSConnection();
652 }
653 if (params?.terminateOpened) {
654 this.terminateWSConnection();
655 }
cda96260 656 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
db2336d9 657 let protocol: string;
edd13439 658 switch (ocppVersion) {
db2336d9 659 case OCPPVersion.VERSION_16:
edd13439
JB
660 case OCPPVersion.VERSION_20:
661 case OCPPVersion.VERSION_201:
44eb6026 662 protocol = `ocpp${ocppVersion}`;
db2336d9
JB
663 break;
664 default:
edd13439 665 this.handleUnsupportedVersion(ocppVersion);
db2336d9
JB
666 break;
667 }
668
56eb297e 669 if (this.isWebSocketConnectionOpened() === true) {
0a03f36c
JB
670 logger.warn(
671 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`
672 );
673 return;
674 }
675
db2336d9 676 logger.info(
0a03f36c 677 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
db2336d9
JB
678 );
679
680 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
681
682 // Handle WebSocket message
683 this.wsConnection.on(
684 'message',
685 this.onMessage.bind(this) as (this: WebSocket, data: RawData, isBinary: boolean) => void
686 );
687 // Handle WebSocket error
688 this.wsConnection.on(
689 'error',
690 this.onError.bind(this) as (this: WebSocket, error: Error) => void
691 );
692 // Handle WebSocket close
693 this.wsConnection.on(
694 'close',
695 this.onClose.bind(this) as (this: WebSocket, code: number, reason: Buffer) => void
696 );
697 // Handle WebSocket open
698 this.wsConnection.on('open', this.onOpen.bind(this) as (this: WebSocket) => void);
699 // Handle WebSocket ping
700 this.wsConnection.on('ping', this.onPing.bind(this) as (this: WebSocket, data: Buffer) => void);
701 // Handle WebSocket pong
702 this.wsConnection.on('pong', this.onPong.bind(this) as (this: WebSocket, data: Buffer) => void);
703 }
704
705 public closeWSConnection(): void {
56eb297e 706 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 707 this.wsConnection?.close();
db2336d9
JB
708 this.wsConnection = null;
709 }
710 }
711
8f879946
JB
712 public startAutomaticTransactionGenerator(
713 connectorIds?: number[],
714 automaticTransactionGeneratorConfiguration?: AutomaticTransactionGeneratorConfiguration
715 ): void {
716 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
717 automaticTransactionGeneratorConfiguration ??
4f69be04 718 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
8f879946
JB
719 this
720 );
53ac516c 721 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc 722 for (const connectorId of connectorIds) {
551e477c 723 this.automaticTransactionGenerator?.startConnector(connectorId);
a5e9befc
JB
724 }
725 } else {
551e477c 726 this.automaticTransactionGenerator?.start();
4f69be04 727 }
1895299d 728 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
729 }
730
a5e9befc 731 public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
53ac516c 732 if (Utils.isNotEmptyArray(connectorIds)) {
a5e9befc
JB
733 for (const connectorId of connectorIds) {
734 this.automaticTransactionGenerator?.stopConnector(connectorId);
735 }
736 } else {
737 this.automaticTransactionGenerator?.stop();
4f69be04 738 }
1895299d 739 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
4f69be04
JB
740 }
741
5e3cb728
JB
742 public async stopTransactionOnConnector(
743 connectorId: number,
744 reason = StopTransactionReason.NONE
745 ): Promise<StopTransactionResponse> {
72092cfc 746 const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
5e3cb728 747 if (
c7e8e0a2
JB
748 this.getBeginEndMeterValues() === true &&
749 this.getOcppStrictCompliance() === true &&
750 this.getOutOfOrderEndMeterValues() === false
5e3cb728
JB
751 ) {
752 // FIXME: Implement OCPP version agnostic helpers
753 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
754 this,
755 connectorId,
756 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
757 );
758 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
759 this,
760 RequestCommand.METER_VALUES,
761 {
762 connectorId,
763 transactionId,
764 meterValue: [transactionEndMeterValue],
765 }
766 );
767 }
768 return this.ocppRequestService.requestHandler<StopTransactionRequest, StopTransactionResponse>(
769 this,
770 RequestCommand.STOP_TRANSACTION,
771 {
772 transactionId,
773 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId, true),
5e3cb728
JB
774 reason,
775 }
776 );
777 }
778
f90c1757 779 private flushMessageBuffer(): void {
8e242273 780 if (this.messageBuffer.size > 0) {
72092cfc 781 this.messageBuffer.forEach((message) => {
1431af78
JB
782 let beginId: string;
783 let commandName: RequestCommand;
8ca6874c 784 const [messageType] = JSON.parse(message) as OutgoingRequest | Response | ErrorResponse;
1431af78
JB
785 const isRequest = messageType === MessageType.CALL_MESSAGE;
786 if (isRequest) {
787 [, , commandName] = JSON.parse(message) as OutgoingRequest;
788 beginId = PerformanceStatistics.beginMeasure(commandName);
789 }
72092cfc 790 this.wsConnection?.send(message);
1431af78 791 isRequest && PerformanceStatistics.endMeasure(commandName, beginId);
8ca6874c
JB
792 logger.debug(
793 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
794 messageType
795 )} payload sent: ${message}`
796 );
8e242273 797 this.messageBuffer.delete(message);
77f00f84
JB
798 });
799 }
800 }
801
1f5df42a
JB
802 private getSupervisionUrlOcppConfiguration(): boolean {
803 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
804 }
805
e8e865ea
JB
806 private getSupervisionUrlOcppKey(): string {
807 return this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl;
808 }
809
72092cfc
JB
810 private getTemplateFromFile(): ChargingStationTemplate | undefined {
811 let template: ChargingStationTemplate;
5ad8570f 812 try {
57adbebc
JB
813 if (this.sharedLRUCache.hasChargingStationTemplate(this.stationInfo?.templateHash)) {
814 template = this.sharedLRUCache.getChargingStationTemplate(this.stationInfo.templateHash);
7c72977b
JB
815 } else {
816 const measureId = `${FileType.ChargingStationTemplate} read`;
817 const beginId = PerformanceStatistics.beginMeasure(measureId);
818 template = JSON.parse(
819 fs.readFileSync(this.templateFile, 'utf8')
820 ) as ChargingStationTemplate;
821 PerformanceStatistics.endMeasure(measureId, beginId);
822 template.templateHash = crypto
823 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
824 .update(JSON.stringify(template))
825 .digest('hex');
57adbebc 826 this.sharedLRUCache.setChargingStationTemplate(template);
7c72977b 827 }
5ad8570f 828 } catch (error) {
e7aeea18 829 FileUtils.handleFileException(
2484ac1e 830 this.templateFile,
7164966d
JB
831 FileType.ChargingStationTemplate,
832 error as NodeJS.ErrnoException,
833 this.logPrefix()
e7aeea18 834 );
5ad8570f 835 }
2484ac1e
JB
836 return template;
837 }
838
7a3a2ebb 839 private getStationInfoFromTemplate(): ChargingStationInfo {
72092cfc 840 const stationTemplate: ChargingStationTemplate | undefined = this.getTemplateFromFile();
fa7bccf4 841 if (Utils.isNullOrUndefined(stationTemplate)) {
eaad6e5c 842 const errorMsg = `Failed to read charging station template file ${this.templateFile}`;
ccb1d6e9
JB
843 logger.error(`${this.logPrefix()} ${errorMsg}`);
844 throw new BaseError(errorMsg);
94ec7e96 845 }
fa7bccf4 846 if (Utils.isEmptyObject(stationTemplate)) {
ccb1d6e9
JB
847 const errorMsg = `Empty charging station information from template file ${this.templateFile}`;
848 logger.error(`${this.logPrefix()} ${errorMsg}`);
849 throw new BaseError(errorMsg);
94ec7e96 850 }
2dcfe98e 851 // Deprecation template keys section
17ac262c 852 ChargingStationUtils.warnDeprecatedTemplateKey(
fa7bccf4 853 stationTemplate,
e7aeea18 854 'supervisionUrl',
17ac262c 855 this.templateFile,
ccb1d6e9 856 this.logPrefix(),
e7aeea18
JB
857 "Use 'supervisionUrls' instead"
858 );
17ac262c 859 ChargingStationUtils.convertDeprecatedTemplateKey(
fa7bccf4 860 stationTemplate,
17ac262c
JB
861 'supervisionUrl',
862 'supervisionUrls'
863 );
fa7bccf4
JB
864 const stationInfo: ChargingStationInfo =
865 ChargingStationUtils.stationTemplateToStationInfo(stationTemplate);
51c83d6f 866 stationInfo.hashId = ChargingStationUtils.getHashId(this.index, stationTemplate);
fa7bccf4
JB
867 stationInfo.chargingStationId = ChargingStationUtils.getChargingStationId(
868 this.index,
869 stationTemplate
870 );
72092cfc 871 stationInfo.ocppVersion = stationTemplate?.ocppVersion ?? OCPPVersion.VERSION_16;
fa7bccf4 872 ChargingStationUtils.createSerialNumber(stationTemplate, stationInfo);
53ac516c 873 if (Utils.isNotEmptyArray(stationTemplate?.power)) {
551e477c 874 stationTemplate.power = stationTemplate.power as number[];
fa7bccf4 875 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationTemplate.power.length);
cc6e8ab5 876 stationInfo.maximumPower =
72092cfc 877 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
878 ? stationTemplate.power[powerArrayRandomIndex] * 1000
879 : stationTemplate.power[powerArrayRandomIndex];
5ad8570f 880 } else {
551e477c 881 stationTemplate.power = stationTemplate?.power as number;
cc6e8ab5 882 stationInfo.maximumPower =
72092cfc 883 stationTemplate?.powerUnit === PowerUnits.KILO_WATT
fa7bccf4
JB
884 ? stationTemplate.power * 1000
885 : stationTemplate.power;
886 }
3637ca2c 887 stationInfo.firmwareVersionPattern =
72092cfc 888 stationTemplate?.firmwareVersionPattern ?? Constants.SEMVER_PATTERN;
3637ca2c 889 if (
5a2a53cf 890 Utils.isNotEmptyString(stationInfo.firmwareVersion) &&
3637ca2c
JB
891 new RegExp(stationInfo.firmwareVersionPattern).test(stationInfo.firmwareVersion) === false
892 ) {
893 logger.warn(
894 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
895 this.templateFile
896 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
897 );
898 }
598c886d 899 stationInfo.firmwareUpgrade = merge<FirmwareUpgrade>(
15748260 900 {
598c886d
JB
901 versionUpgrade: {
902 step: 1,
903 },
15748260
JB
904 reset: true,
905 },
72092cfc 906 stationTemplate?.firmwareUpgrade ?? {}
15748260 907 );
d812bdcb 908 stationInfo.resetTime = !Utils.isNullOrUndefined(stationTemplate?.resetTime)
fa7bccf4 909 ? stationTemplate.resetTime * 1000
e7aeea18 910 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
c72f6634
JB
911 const configuredMaxConnectors =
912 ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
fa7bccf4
JB
913 ChargingStationUtils.checkConfiguredMaxConnectors(
914 configuredMaxConnectors,
915 this.templateFile,
fc040c43 916 this.logPrefix()
fa7bccf4
JB
917 );
918 const templateMaxConnectors =
919 ChargingStationUtils.getTemplateMaxNumberOfConnectors(stationTemplate);
920 ChargingStationUtils.checkTemplateMaxConnectors(
921 templateMaxConnectors,
922 this.templateFile,
fc040c43 923 this.logPrefix()
fa7bccf4
JB
924 );
925 if (
926 configuredMaxConnectors >
927 (stationTemplate?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
928 !stationTemplate?.randomConnectors
929 ) {
930 logger.warn(
931 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
932 this.templateFile
933 }, forcing random connector configurations affectation`
934 );
935 stationInfo.randomConnectors = true;
936 }
937 // Build connectors if needed (FIXME: should be factored out)
938 this.initializeConnectors(stationInfo, configuredMaxConnectors, templateMaxConnectors);
939 stationInfo.maximumAmperage = this.getMaximumAmperage(stationInfo);
940 ChargingStationUtils.createStationInfoHash(stationInfo);
9ac86a7e 941 return stationInfo;
5ad8570f
JB
942 }
943
551e477c
JB
944 private getStationInfoFromFile(): ChargingStationInfo | undefined {
945 let stationInfo: ChargingStationInfo | undefined;
fa7bccf4 946 this.getStationInfoPersistentConfiguration() &&
551e477c 947 (stationInfo = this.getConfigurationFromFile()?.stationInfo);
fa7bccf4 948 stationInfo && ChargingStationUtils.createStationInfoHash(stationInfo);
f765beaa 949 return stationInfo;
2484ac1e
JB
950 }
951
952 private getStationInfo(): ChargingStationInfo {
953 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
551e477c 954 const stationInfoFromFile: ChargingStationInfo | undefined = this.getStationInfoFromFile();
aca53a1a 955 // Priority: charging station info from template > charging station info from configuration file > charging station info attribute
f765beaa 956 if (stationInfoFromFile?.templateHash === stationInfoFromTemplate.templateHash) {
01efc60a
JB
957 if (this.stationInfo?.infoHash === stationInfoFromFile?.infoHash) {
958 return this.stationInfo;
959 }
2484ac1e 960 return stationInfoFromFile;
f765beaa 961 }
fec4d204
JB
962 stationInfoFromFile &&
963 ChargingStationUtils.propagateSerialNumber(
964 this.getTemplateFromFile(),
965 stationInfoFromFile,
966 stationInfoFromTemplate
967 );
01efc60a 968 return stationInfoFromTemplate;
2484ac1e
JB
969 }
970
971 private saveStationInfo(): void {
ccb1d6e9 972 if (this.getStationInfoPersistentConfiguration()) {
7c72977b 973 this.saveConfiguration();
ccb1d6e9 974 }
2484ac1e
JB
975 }
976
e8e865ea 977 private getOcppPersistentConfiguration(): boolean {
ccb1d6e9
JB
978 return this.stationInfo?.ocppPersistentConfiguration ?? true;
979 }
980
981 private getStationInfoPersistentConfiguration(): boolean {
982 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
e8e865ea
JB
983 }
984
c0560973 985 private handleUnsupportedVersion(version: OCPPVersion) {
fc040c43
JB
986 const errMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
987 logger.error(`${this.logPrefix()} ${errMsg}`);
6c8f5d90 988 throw new BaseError(errMsg);
c0560973
JB
989 }
990
2484ac1e 991 private initialize(): void {
fa7bccf4 992 this.configurationFile = path.join(
ee5f26a2 993 path.dirname(this.templateFile.replace('station-templates', 'configurations')),
44eb6026 994 `${ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile())}.json`
0642c3d2 995 );
b44b779a
JB
996 this.stationInfo = this.getStationInfo();
997 this.saveStationInfo();
7a3a2ebb 998 // Avoid duplication of connectors related information in RAM
94ec7e96 999 this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
fa7bccf4 1000 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
ad774cec 1001 if (this.getEnableStatistics() === true) {
0642c3d2 1002 this.performanceStatistics = PerformanceStatistics.getInstance(
51c83d6f 1003 this.stationInfo.hashId,
0642c3d2 1004 this.stationInfo.chargingStationId,
fa7bccf4 1005 this.configuredSupervisionUrl
0642c3d2
JB
1006 );
1007 }
fa7bccf4
JB
1008 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
1009 this.stationInfo
1010 );
fa7bccf4
JB
1011 this.powerDivider = this.getPowerDivider();
1012 // OCPP configuration
1013 this.ocppConfiguration = this.getOcppConfiguration();
1014 this.initializeOcppConfiguration();
cda96260
JB
1015 const ocppVersion = this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
1016 switch (ocppVersion) {
c0560973 1017 case OCPPVersion.VERSION_16:
e7aeea18 1018 this.ocppIncomingRequestService =
08f130a0 1019 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
e7aeea18 1020 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
08f130a0 1021 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
e7aeea18 1022 );
c0560973 1023 break;
edd13439
JB
1024 case OCPPVersion.VERSION_20:
1025 case OCPPVersion.VERSION_201:
1026 this.ocppIncomingRequestService =
1027 OCPP20IncomingRequestService.getInstance<OCPP20IncomingRequestService>();
1028 this.ocppRequestService = OCPP20RequestService.getInstance<OCPP20RequestService>(
1029 OCPP20ResponseService.getInstance<OCPP20ResponseService>()
1030 );
1031 break;
c0560973 1032 default:
cda96260 1033 this.handleUnsupportedVersion(ocppVersion);
c0560973
JB
1034 break;
1035 }
b7f9e41d 1036 if (this.stationInfo?.autoRegister === true) {
47e22477 1037 this.bootNotificationResponse = {
d270cc87 1038 currentTime: new Date(),
47e22477 1039 interval: this.getHeartbeatInterval() / 1000,
d270cc87 1040 status: RegistrationStatusEnumType.ACCEPTED,
47e22477
JB
1041 };
1042 }
3637ca2c
JB
1043 if (
1044 this.stationInfo.firmwareStatus === FirmwareStatus.Installing &&
5a2a53cf
JB
1045 Utils.isNotEmptyString(this.stationInfo.firmwareVersion) &&
1046 Utils.isNotEmptyString(this.stationInfo.firmwareVersionPattern)
3637ca2c 1047 ) {
d812bdcb 1048 const patternGroup: number | undefined =
15748260 1049 this.stationInfo.firmwareUpgrade?.versionUpgrade?.patternGroup ??
d812bdcb 1050 this.stationInfo.firmwareVersion?.split('.').length;
72092cfc
JB
1051 const match = this.stationInfo?.firmwareVersion
1052 ?.match(new RegExp(this.stationInfo.firmwareVersionPattern))
1053 ?.slice(1, patternGroup + 1);
3637ca2c 1054 const patchLevelIndex = match.length - 1;
5d280aae 1055 match[patchLevelIndex] = (
07c52a72
JB
1056 Utils.convertToInt(match[patchLevelIndex]) +
1057 this.stationInfo.firmwareUpgrade?.versionUpgrade?.step
5d280aae 1058 ).toString();
72092cfc 1059 this.stationInfo.firmwareVersion = match?.join('.');
3637ca2c 1060 }
147d0e0f
JB
1061 }
1062
2484ac1e 1063 private initializeOcppConfiguration(): void {
17ac262c
JB
1064 if (
1065 !ChargingStationConfigurationUtils.getConfigurationKey(
1066 this,
1067 StandardParametersKey.HeartbeatInterval
1068 )
1069 ) {
1070 ChargingStationConfigurationUtils.addConfigurationKey(
1071 this,
1072 StandardParametersKey.HeartbeatInterval,
1073 '0'
1074 );
f0f65a62 1075 }
17ac262c
JB
1076 if (
1077 !ChargingStationConfigurationUtils.getConfigurationKey(
1078 this,
1079 StandardParametersKey.HeartBeatInterval
1080 )
1081 ) {
1082 ChargingStationConfigurationUtils.addConfigurationKey(
1083 this,
1084 StandardParametersKey.HeartBeatInterval,
1085 '0',
1086 { visible: false }
1087 );
f0f65a62 1088 }
e7aeea18
JB
1089 if (
1090 this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1091 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e7aeea18 1092 ) {
17ac262c
JB
1093 ChargingStationConfigurationUtils.addConfigurationKey(
1094 this,
a59737e3 1095 this.getSupervisionUrlOcppKey(),
fa7bccf4 1096 this.configuredSupervisionUrl.href,
e7aeea18
JB
1097 { reboot: true }
1098 );
e6895390
JB
1099 } else if (
1100 !this.getSupervisionUrlOcppConfiguration() &&
17ac262c 1101 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
e6895390 1102 ) {
17ac262c
JB
1103 ChargingStationConfigurationUtils.deleteConfigurationKey(
1104 this,
1105 this.getSupervisionUrlOcppKey(),
1106 { save: false }
1107 );
12fc74d6 1108 }
cc6e8ab5 1109 if (
5a2a53cf 1110 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1111 !ChargingStationConfigurationUtils.getConfigurationKey(
1112 this,
1113 this.stationInfo.amperageLimitationOcppKey
1114 )
cc6e8ab5 1115 ) {
17ac262c
JB
1116 ChargingStationConfigurationUtils.addConfigurationKey(
1117 this,
cc6e8ab5 1118 this.stationInfo.amperageLimitationOcppKey,
17ac262c
JB
1119 (
1120 this.stationInfo.maximumAmperage *
1121 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1122 ).toString()
cc6e8ab5
JB
1123 );
1124 }
17ac262c
JB
1125 if (
1126 !ChargingStationConfigurationUtils.getConfigurationKey(
1127 this,
1128 StandardParametersKey.SupportedFeatureProfiles
1129 )
1130 ) {
1131 ChargingStationConfigurationUtils.addConfigurationKey(
1132 this,
e7aeea18 1133 StandardParametersKey.SupportedFeatureProfiles,
b22787b4 1134 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
e7aeea18
JB
1135 );
1136 }
17ac262c
JB
1137 ChargingStationConfigurationUtils.addConfigurationKey(
1138 this,
e7aeea18
JB
1139 StandardParametersKey.NumberOfConnectors,
1140 this.getNumberOfConnectors().toString(),
a95873d8
JB
1141 { readonly: true },
1142 { overwrite: true }
e7aeea18 1143 );
17ac262c
JB
1144 if (
1145 !ChargingStationConfigurationUtils.getConfigurationKey(
1146 this,
1147 StandardParametersKey.MeterValuesSampledData
1148 )
1149 ) {
1150 ChargingStationConfigurationUtils.addConfigurationKey(
1151 this,
e7aeea18
JB
1152 StandardParametersKey.MeterValuesSampledData,
1153 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1154 );
7abfea5f 1155 }
17ac262c
JB
1156 if (
1157 !ChargingStationConfigurationUtils.getConfigurationKey(
1158 this,
1159 StandardParametersKey.ConnectorPhaseRotation
1160 )
1161 ) {
7e1dc878 1162 const connectorPhaseRotation = [];
734d790d 1163 for (const connectorId of this.connectors.keys()) {
7e1dc878 1164 // AC/DC
734d790d
JB
1165 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1166 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1167 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1168 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
e7aeea18 1169 // AC
734d790d
JB
1170 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1171 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1172 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1173 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
7e1dc878
JB
1174 }
1175 }
17ac262c
JB
1176 ChargingStationConfigurationUtils.addConfigurationKey(
1177 this,
e7aeea18
JB
1178 StandardParametersKey.ConnectorPhaseRotation,
1179 connectorPhaseRotation.toString()
1180 );
7e1dc878 1181 }
e7aeea18 1182 if (
17ac262c
JB
1183 !ChargingStationConfigurationUtils.getConfigurationKey(
1184 this,
1185 StandardParametersKey.AuthorizeRemoteTxRequests
e7aeea18
JB
1186 )
1187 ) {
17ac262c
JB
1188 ChargingStationConfigurationUtils.addConfigurationKey(
1189 this,
1190 StandardParametersKey.AuthorizeRemoteTxRequests,
1191 'true'
1192 );
36f6a92e 1193 }
17ac262c
JB
1194 if (
1195 !ChargingStationConfigurationUtils.getConfigurationKey(
1196 this,
1197 StandardParametersKey.LocalAuthListEnabled
1198 ) &&
1199 ChargingStationConfigurationUtils.getConfigurationKey(
1200 this,
1201 StandardParametersKey.SupportedFeatureProfiles
72092cfc 1202 )?.value?.includes(SupportedFeatureProfiles.LocalAuthListManagement)
17ac262c
JB
1203 ) {
1204 ChargingStationConfigurationUtils.addConfigurationKey(
1205 this,
1206 StandardParametersKey.LocalAuthListEnabled,
1207 'false'
1208 );
1209 }
1210 if (
1211 !ChargingStationConfigurationUtils.getConfigurationKey(
1212 this,
1213 StandardParametersKey.ConnectionTimeOut
1214 )
1215 ) {
1216 ChargingStationConfigurationUtils.addConfigurationKey(
1217 this,
e7aeea18
JB
1218 StandardParametersKey.ConnectionTimeOut,
1219 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1220 );
8bce55bf 1221 }
2484ac1e 1222 this.saveOcppConfiguration();
073bd098
JB
1223 }
1224
3d25cc86
JB
1225 private initializeConnectors(
1226 stationInfo: ChargingStationInfo,
fa7bccf4 1227 configuredMaxConnectors: number,
3d25cc86
JB
1228 templateMaxConnectors: number
1229 ): void {
1230 if (!stationInfo?.Connectors && this.connectors.size === 0) {
fc040c43
JB
1231 const logMsg = `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1232 logger.error(`${this.logPrefix()} ${logMsg}`);
3d25cc86
JB
1233 throw new BaseError(logMsg);
1234 }
1235 if (!stationInfo?.Connectors[0]) {
1236 logger.warn(
1237 `${this.logPrefix()} Charging station information from template ${
1238 this.templateFile
1239 } with no connector Id 0 configuration`
1240 );
1241 }
1242 if (stationInfo?.Connectors) {
1243 const connectorsConfigHash = crypto
1244 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
14ecae6a 1245 .update(`${JSON.stringify(stationInfo?.Connectors)}${configuredMaxConnectors.toString()}`)
3d25cc86
JB
1246 .digest('hex');
1247 const connectorsConfigChanged =
1248 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1249 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1250 connectorsConfigChanged && this.connectors.clear();
1251 this.connectorsConfigurationHash = connectorsConfigHash;
1252 // Add connector Id 0
1253 let lastConnector = '0';
1254 for (lastConnector in stationInfo?.Connectors) {
56eb297e 1255 const connectorStatus = stationInfo?.Connectors[lastConnector];
3d25cc86
JB
1256 const lastConnectorId = Utils.convertToInt(lastConnector);
1257 if (
1258 lastConnectorId === 0 &&
bb83b5ed 1259 this.getUseConnectorId0(stationInfo) === true &&
56eb297e 1260 connectorStatus
3d25cc86 1261 ) {
56eb297e 1262 this.checkStationInfoConnectorStatus(lastConnectorId, connectorStatus);
3d25cc86
JB
1263 this.connectors.set(
1264 lastConnectorId,
56eb297e 1265 Utils.cloneObject<ConnectorStatus>(connectorStatus)
3d25cc86
JB
1266 );
1267 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1268 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1269 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1270 }
1271 }
1272 }
1273 // Generate all connectors
1274 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
fa7bccf4 1275 for (let index = 1; index <= configuredMaxConnectors; index++) {
ccb1d6e9 1276 const randConnectorId = stationInfo?.randomConnectors
3d25cc86
JB
1277 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1278 : index;
56eb297e
JB
1279 const connectorStatus = stationInfo?.Connectors[randConnectorId.toString()];
1280 this.checkStationInfoConnectorStatus(randConnectorId, connectorStatus);
1281 this.connectors.set(index, Utils.cloneObject<ConnectorStatus>(connectorStatus));
3d25cc86
JB
1282 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1283 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1284 this.getConnectorStatus(index).chargingProfiles = [];
1285 }
1286 }
1287 }
1288 }
1289 } else {
1290 logger.warn(
1291 `${this.logPrefix()} Charging station information from template ${
1292 this.templateFile
1293 } with no connectors configuration defined, using already defined connectors`
1294 );
1295 }
1296 // Initialize transaction attributes on connectors
1297 for (const connectorId of this.connectors.keys()) {
72092cfc 1298 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
f5b51071
JB
1299 logger.warn(
1300 `${this.logPrefix()} Connector ${connectorId} at initialization has a transaction started: ${
72092cfc 1301 this.getConnectorStatus(connectorId)?.transactionId
f5b51071
JB
1302 }`
1303 );
1304 }
1984f194
JB
1305 if (
1306 connectorId > 0 &&
72092cfc
JB
1307 (this.getConnectorStatus(connectorId)?.transactionStarted === undefined ||
1308 this.getConnectorStatus(connectorId)?.transactionStarted === null)
1984f194 1309 ) {
3d25cc86
JB
1310 this.initializeConnectorStatus(connectorId);
1311 }
1312 }
1313 }
1314
56eb297e
JB
1315 private checkStationInfoConnectorStatus(
1316 connectorId: number,
1317 connectorStatus: ConnectorStatus
1318 ): void {
1319 if (!Utils.isNullOrUndefined(connectorStatus?.status)) {
1320 logger.warn(
1321 `${this.logPrefix()} Charging station information from template ${
1322 this.templateFile
1323 } with connector ${connectorId} status configuration defined, undefine it`
1324 );
1325 connectorStatus.status = undefined;
1326 }
1327 }
1328
551e477c
JB
1329 private getConfigurationFromFile(): ChargingStationConfiguration | undefined {
1330 let configuration: ChargingStationConfiguration | undefined;
2484ac1e 1331 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1332 try {
57adbebc
JB
1333 if (this.sharedLRUCache.hasChargingStationConfiguration(this.configurationFileHash)) {
1334 configuration = this.sharedLRUCache.getChargingStationConfiguration(
1335 this.configurationFileHash
1336 );
7c72977b
JB
1337 } else {
1338 const measureId = `${FileType.ChargingStationConfiguration} read`;
1339 const beginId = PerformanceStatistics.beginMeasure(measureId);
1340 configuration = JSON.parse(
1341 fs.readFileSync(this.configurationFile, 'utf8')
1342 ) as ChargingStationConfiguration;
1343 PerformanceStatistics.endMeasure(measureId, beginId);
1344 this.configurationFileHash = configuration.configurationHash;
57adbebc 1345 this.sharedLRUCache.setChargingStationConfiguration(configuration);
7c72977b 1346 }
073bd098
JB
1347 } catch (error) {
1348 FileUtils.handleFileException(
073bd098 1349 this.configurationFile,
7164966d
JB
1350 FileType.ChargingStationConfiguration,
1351 error as NodeJS.ErrnoException,
1352 this.logPrefix()
073bd098
JB
1353 );
1354 }
1355 }
1356 return configuration;
1357 }
1358
7c72977b 1359 private saveConfiguration(): void {
2484ac1e
JB
1360 if (this.configurationFile) {
1361 try {
2484ac1e
JB
1362 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1363 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1364 }
ccb1d6e9
JB
1365 const configurationData: ChargingStationConfiguration =
1366 this.getConfigurationFromFile() ?? {};
7c72977b
JB
1367 this.ocppConfiguration?.configurationKey &&
1368 (configurationData.configurationKey = this.ocppConfiguration.configurationKey);
1369 this.stationInfo && (configurationData.stationInfo = this.stationInfo);
1370 delete configurationData.configurationHash;
1371 const configurationHash = crypto
1372 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1373 .update(JSON.stringify(configurationData))
1374 .digest('hex');
1375 if (this.configurationFileHash !== configurationHash) {
1376 configurationData.configurationHash = configurationHash;
1377 const measureId = `${FileType.ChargingStationConfiguration} write`;
1378 const beginId = PerformanceStatistics.beginMeasure(measureId);
1379 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1380 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1381 fs.closeSync(fileDescriptor);
1382 PerformanceStatistics.endMeasure(measureId, beginId);
57adbebc 1383 this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
7c72977b 1384 this.configurationFileHash = configurationHash;
57adbebc 1385 this.sharedLRUCache.setChargingStationConfiguration(configurationData);
7c72977b
JB
1386 } else {
1387 logger.debug(
1388 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1389 this.configurationFile
1390 }`
1391 );
2484ac1e 1392 }
2484ac1e
JB
1393 } catch (error) {
1394 FileUtils.handleFileException(
2484ac1e 1395 this.configurationFile,
7164966d
JB
1396 FileType.ChargingStationConfiguration,
1397 error as NodeJS.ErrnoException,
1398 this.logPrefix()
073bd098
JB
1399 );
1400 }
2484ac1e
JB
1401 } else {
1402 logger.error(
01efc60a 1403 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
2484ac1e 1404 );
073bd098
JB
1405 }
1406 }
1407
551e477c
JB
1408 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | undefined {
1409 return this.getTemplateFromFile()?.Configuration;
2484ac1e
JB
1410 }
1411
551e477c
JB
1412 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | undefined {
1413 let configuration: ChargingStationConfiguration | undefined;
23290150 1414 if (this.getOcppPersistentConfiguration() === true) {
7a3a2ebb
JB
1415 const configurationFromFile = this.getConfigurationFromFile();
1416 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1417 }
2484ac1e 1418 configuration && delete configuration.stationInfo;
073bd098 1419 return configuration;
7dde0b73
JB
1420 }
1421
551e477c
JB
1422 private getOcppConfiguration(): ChargingStationOcppConfiguration | undefined {
1423 let ocppConfiguration: ChargingStationOcppConfiguration | undefined =
72092cfc 1424 this.getOcppConfigurationFromFile();
2484ac1e
JB
1425 if (!ocppConfiguration) {
1426 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1427 }
1428 return ocppConfiguration;
1429 }
1430
c0560973 1431 private async onOpen(): Promise<void> {
56eb297e 1432 if (this.isWebSocketConnectionOpened() === true) {
5144f4d1
JB
1433 logger.info(
1434 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1435 );
ed6cfcff 1436 if (this.isRegistered() === false) {
5144f4d1
JB
1437 // Send BootNotification
1438 let registrationRetryCount = 0;
1439 do {
f7f98c68 1440 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
5144f4d1
JB
1441 BootNotificationRequest,
1442 BootNotificationResponse
8bfbc743
JB
1443 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1444 skipBufferingOnError: true,
1445 });
ed6cfcff 1446 if (this.isRegistered() === false) {
5144f4d1
JB
1447 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1448 await Utils.sleep(
1895299d 1449 this?.bootNotificationResponse?.interval
5144f4d1
JB
1450 ? this.bootNotificationResponse.interval * 1000
1451 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1452 );
1453 }
1454 } while (
ed6cfcff 1455 this.isRegistered() === false &&
5144f4d1
JB
1456 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1457 this.getRegistrationMaxRetries() === -1)
1458 );
1459 }
ed6cfcff 1460 if (this.isRegistered() === true) {
23290150 1461 if (this.isInAcceptedState() === true) {
94bb24d5 1462 await this.startMessageSequence();
c0560973 1463 }
5144f4d1
JB
1464 } else {
1465 logger.error(
1466 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1467 );
caad9d6b 1468 }
5144f4d1 1469 this.wsConnectionRestarted = false;
aa428a31 1470 this.autoReconnectRetryCount = 0;
1895299d 1471 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966 1472 } else {
5144f4d1
JB
1473 logger.warn(
1474 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
e7aeea18 1475 );
2e6f5966 1476 }
2e6f5966
JB
1477 }
1478
ef7d8c21 1479 private async onClose(code: number, reason: Buffer): Promise<void> {
d09085e9 1480 switch (code) {
6c65a295
JB
1481 // Normal close
1482 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1483 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18 1484 logger.info(
5e3cb728 1485 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1486 code
ef7d8c21 1487 )}' and reason '${reason.toString()}'`
e7aeea18 1488 );
c0560973
JB
1489 this.autoReconnectRetryCount = 0;
1490 break;
6c65a295
JB
1491 // Abnormal close
1492 default:
e7aeea18 1493 logger.error(
5e3cb728 1494 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
e7aeea18 1495 code
ef7d8c21 1496 )}' and reason '${reason.toString()}'`
e7aeea18 1497 );
56eb297e 1498 this.started === true && (await this.reconnect());
c0560973
JB
1499 break;
1500 }
1895299d 1501 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
2e6f5966
JB
1502 }
1503
ef7d8c21 1504 private async onMessage(data: RawData): Promise<void> {
b3ec7bc1
JB
1505 let messageType: number;
1506 let messageId: string;
1507 let commandName: IncomingRequestCommand;
1508 let commandPayload: JsonType;
1509 let errorType: ErrorType;
1510 let errorMessage: string;
1511 let errorDetails: JsonType;
d900c8d7
JB
1512 let responseCallback: ResponseCallback;
1513 let errorCallback: ErrorCallback;
32b02249 1514 let requestCommandName: RequestCommand | IncomingRequestCommand;
b3ec7bc1 1515 let requestPayload: JsonType;
32b02249 1516 let cachedRequest: CachedRequest;
c0560973
JB
1517 let errMsg: string;
1518 try {
b3ec7bc1 1519 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
53e5fd67 1520 if (Array.isArray(request) === true) {
9934652c 1521 [messageType, messageId] = request;
b3ec7bc1
JB
1522 // Check the type of message
1523 switch (messageType) {
1524 // Incoming Message
1525 case MessageType.CALL_MESSAGE:
9934652c 1526 [, , commandName, commandPayload] = request as IncomingRequest;
0638ddd2 1527 if (this.getEnableStatistics() === true) {
551e477c 1528 this.performanceStatistics?.addRequestStatistic(commandName, messageType);
b3ec7bc1
JB
1529 }
1530 logger.debug(
1531 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1532 request
1533 )}`
1534 );
1535 // Process the message
1536 await this.ocppIncomingRequestService.incomingRequestHandler(
08f130a0 1537 this,
b3ec7bc1
JB
1538 messageId,
1539 commandName,
1540 commandPayload
1541 );
1542 break;
1543 // Outcome Message
1544 case MessageType.CALL_RESULT_MESSAGE:
9934652c 1545 [, , commandPayload] = request as Response;
ba7965c4 1546 if (this.requests.has(messageId) === false) {
a2d1c0f1
JB
1547 // Error
1548 throw new OCPPError(
1549 ErrorType.INTERNAL_ERROR,
1550 `Response for unknown message id ${messageId}`,
72092cfc 1551 undefined,
a2d1c0f1
JB
1552 commandPayload
1553 );
1554 }
b3ec7bc1
JB
1555 // Respond
1556 cachedRequest = this.requests.get(messageId);
53e5fd67 1557 if (Array.isArray(cachedRequest) === true) {
53e07f94 1558 [responseCallback, errorCallback, requestCommandName, requestPayload] = cachedRequest;
b3ec7bc1
JB
1559 } else {
1560 throw new OCPPError(
1561 ErrorType.PROTOCOL_ERROR,
53e5fd67 1562 `Cached request for message id ${messageId} response is not an array`,
72092cfc 1563 undefined,
c2bc716f 1564 cachedRequest as unknown as JsonType
b3ec7bc1
JB
1565 );
1566 }
1567 logger.debug(
7ec6c5c9 1568 `${this.logPrefix()} << Command '${
2a232a18 1569 requestCommandName ?? Constants.UNKNOWN_COMMAND
7ec6c5c9 1570 }' received response payload: ${JSON.stringify(request)}`
b3ec7bc1 1571 );
a2d1c0f1
JB
1572 responseCallback(commandPayload, requestPayload);
1573 break;
1574 // Error Message
1575 case MessageType.CALL_ERROR_MESSAGE:
1576 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
ba7965c4 1577 if (this.requests.has(messageId) === false) {
b3ec7bc1
JB
1578 // Error
1579 throw new OCPPError(
1580 ErrorType.INTERNAL_ERROR,
a2d1c0f1 1581 `Error response for unknown message id ${messageId}`,
72092cfc 1582 undefined,
a2d1c0f1 1583 { errorType, errorMessage, errorDetails }
b3ec7bc1
JB
1584 );
1585 }
b3ec7bc1 1586 cachedRequest = this.requests.get(messageId);
53e5fd67 1587 if (Array.isArray(cachedRequest) === true) {
a2d1c0f1 1588 [, errorCallback, requestCommandName] = cachedRequest;
b3ec7bc1
JB
1589 } else {
1590 throw new OCPPError(
1591 ErrorType.PROTOCOL_ERROR,
53e5fd67 1592 `Cached request for message id ${messageId} error response is not an array`,
72092cfc 1593 undefined,
c2bc716f 1594 cachedRequest as unknown as JsonType
b3ec7bc1
JB
1595 );
1596 }
1597 logger.debug(
7ec6c5c9 1598 `${this.logPrefix()} << Command '${
2a232a18 1599 requestCommandName ?? Constants.UNKNOWN_COMMAND
2cc5d5ec 1600 }' received error response payload: ${JSON.stringify(request)}`
b3ec7bc1 1601 );
a2d1c0f1 1602 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
b3ec7bc1
JB
1603 break;
1604 // Error
1605 default:
1606 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
fc040c43
JB
1607 errMsg = `Wrong message type ${messageType}`;
1608 logger.error(`${this.logPrefix()} ${errMsg}`);
b3ec7bc1
JB
1609 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1610 }
1895299d 1611 parentPort?.postMessage(MessageChannelUtils.buildUpdatedMessage(this));
47e22477 1612 } else {
53e5fd67 1613 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not an array', null, {
ba7965c4 1614 request,
ac54a9bb 1615 });
47e22477 1616 }
c0560973
JB
1617 } catch (error) {
1618 // Log
e7aeea18 1619 logger.error(
91a4f151 1620 `${this.logPrefix()} Incoming OCPP command '${
2a232a18 1621 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
9c5d9fa4
JB
1622 }' message '${data.toString()}'${
1623 messageType !== MessageType.CALL_MESSAGE
1624 ? ` matching cached request '${JSON.stringify(this.requests.get(messageId))}'`
1625 : ''
1626 } processing error:`,
e7aeea18
JB
1627 error
1628 );
ba7965c4 1629 if (error instanceof OCPPError === false) {
247659af 1630 logger.warn(
91a4f151 1631 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2a232a18 1632 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
fc040c43 1633 }' message '${data.toString()}' handling is not an OCPPError:`,
247659af
JB
1634 error
1635 );
1636 }
13701f69
JB
1637 switch (messageType) {
1638 case MessageType.CALL_MESSAGE:
1639 // Send error
1640 await this.ocppRequestService.sendError(
1641 this,
1642 messageId,
1643 error as OCPPError,
1644 commandName ?? requestCommandName ?? null
1645 );
1646 break;
1647 case MessageType.CALL_RESULT_MESSAGE:
1648 case MessageType.CALL_ERROR_MESSAGE:
1649 if (errorCallback) {
1650 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1651 errorCallback(error as OCPPError, false);
1652 } else {
1653 // Remove the request from the cache in case of error at response handling
1654 this.requests.delete(messageId);
1655 }
de4cb8b6 1656 break;
ba7965c4 1657 }
c0560973 1658 }
2328be1e
JB
1659 }
1660
c0560973 1661 private onPing(): void {
44eb6026 1662 logger.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
c0560973
JB
1663 }
1664
1665 private onPong(): void {
44eb6026 1666 logger.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
c0560973
JB
1667 }
1668
9534e74e 1669 private onError(error: WSError): void {
bcc9c3c0 1670 this.closeWSConnection();
44eb6026 1671 logger.error(`${this.logPrefix()} WebSocket error:`, error);
c0560973
JB
1672 }
1673
18bf8274 1674 private getEnergyActiveImportRegister(connectorStatus: ConnectorStatus, rounded = false): number {
95bdbf12 1675 if (this.getMeteringPerTransaction() === true) {
07989fad 1676 return (
18bf8274 1677 (rounded === true
07989fad
JB
1678 ? Math.round(connectorStatus?.transactionEnergyActiveImportRegisterValue)
1679 : connectorStatus?.transactionEnergyActiveImportRegisterValue) ?? 0
1680 );
1681 }
1682 return (
18bf8274 1683 (rounded === true
07989fad
JB
1684 ? Math.round(connectorStatus?.energyActiveImportRegisterValue)
1685 : connectorStatus?.energyActiveImportRegisterValue) ?? 0
1686 );
1687 }
1688
bb83b5ed 1689 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean {
fa7bccf4 1690 const localStationInfo = stationInfo ?? this.stationInfo;
a14885a3 1691 return localStationInfo?.useConnectorId0 ?? true;
8bce55bf
JB
1692 }
1693
60ddad53
JB
1694 private getNumberOfRunningTransactions(): number {
1695 let trxCount = 0;
1696 for (const connectorId of this.connectors.keys()) {
1697 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1698 trxCount++;
1699 }
1700 }
1701 return trxCount;
1702 }
1703
1704 private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
1705 for (const connectorId of this.connectors.keys()) {
1706 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted === true) {
1707 await this.stopTransactionOnConnector(connectorId, reason);
1708 }
1709 }
1710 }
1711
1f761b9a 1712 // 0 for disabling
c72f6634 1713 private getConnectionTimeout(): number {
17ac262c
JB
1714 if (
1715 ChargingStationConfigurationUtils.getConfigurationKey(
1716 this,
1717 StandardParametersKey.ConnectionTimeOut
1718 )
1719 ) {
e7aeea18 1720 return (
17ac262c
JB
1721 parseInt(
1722 ChargingStationConfigurationUtils.getConfigurationKey(
1723 this,
1724 StandardParametersKey.ConnectionTimeOut
1725 ).value
1726 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
e7aeea18 1727 );
291cb255 1728 }
291cb255 1729 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1730 }
1731
1f761b9a 1732 // -1 for unlimited, 0 for disabling
72092cfc 1733 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1734 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1735 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1736 }
1737 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1738 return Configuration.getAutoReconnectMaxRetries();
1739 }
1740 return -1;
1741 }
1742
ec977daf 1743 // 0 for disabling
72092cfc 1744 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1745 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1746 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1747 }
1748 return -1;
1749 }
1750
c0560973
JB
1751 private getPowerDivider(): number {
1752 let powerDivider = this.getNumberOfConnectors();
fa7bccf4 1753 if (this.stationInfo?.powerSharedByConnectors) {
c0560973 1754 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1755 }
1756 return powerDivider;
1757 }
1758
fa7bccf4
JB
1759 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1760 const maximumPower = this.getMaximumPower(stationInfo);
1761 switch (this.getCurrentOutType(stationInfo)) {
cc6e8ab5
JB
1762 case CurrentType.AC:
1763 return ACElectricUtils.amperagePerPhaseFromPower(
fa7bccf4 1764 this.getNumberOfPhases(stationInfo),
ad8537a7 1765 maximumPower / this.getNumberOfConnectors(),
fa7bccf4 1766 this.getVoltageOut(stationInfo)
cc6e8ab5
JB
1767 );
1768 case CurrentType.DC:
fa7bccf4 1769 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
cc6e8ab5
JB
1770 }
1771 }
1772
cc6e8ab5
JB
1773 private getAmperageLimitation(): number | undefined {
1774 if (
5a2a53cf 1775 Utils.isNotEmptyString(this.stationInfo?.amperageLimitationOcppKey) &&
17ac262c
JB
1776 ChargingStationConfigurationUtils.getConfigurationKey(
1777 this,
1778 this.stationInfo.amperageLimitationOcppKey
1779 )
cc6e8ab5
JB
1780 ) {
1781 return (
1782 Utils.convertToInt(
17ac262c
JB
1783 ChargingStationConfigurationUtils.getConfigurationKey(
1784 this,
1785 this.stationInfo.amperageLimitationOcppKey
72092cfc 1786 )?.value
17ac262c 1787 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
cc6e8ab5
JB
1788 );
1789 }
1790 }
1791
c0560973 1792 private async startMessageSequence(): Promise<void> {
b7f9e41d 1793 if (this.stationInfo?.autoRegister === true) {
f7f98c68 1794 await this.ocppRequestService.requestHandler<
ef6fa3fb
JB
1795 BootNotificationRequest,
1796 BootNotificationResponse
8bfbc743
JB
1797 >(this, RequestCommand.BOOT_NOTIFICATION, this.bootNotificationRequest, {
1798 skipBufferingOnError: true,
1799 });
6114e6f1 1800 }
136c90ba 1801 // Start WebSocket ping
c0560973 1802 this.startWebSocketPing();
5ad8570f 1803 // Start heartbeat
c0560973 1804 this.startHeartbeat();
0a60c33c 1805 // Initialize connectors status
734d790d 1806 for (const connectorId of this.connectors.keys()) {
72092cfc 1807 let connectorStatus: ConnectorStatusEnum | undefined;
734d790d 1808 if (connectorId === 0) {
593cf3f9 1809 continue;
e7aeea18 1810 } else if (
56eb297e
JB
1811 !this.getConnectorStatus(connectorId)?.status &&
1812 (this.isChargingStationAvailable() === false ||
1789ba2c 1813 this.isConnectorAvailable(connectorId) === false)
e7aeea18 1814 ) {
6e939d9e 1815 connectorStatus = ConnectorStatusEnum.UNAVAILABLE;
45c0ae82
JB
1816 } else if (
1817 !this.getConnectorStatus(connectorId)?.status &&
1818 this.getConnectorStatus(connectorId)?.bootStatus
1819 ) {
1820 // Set boot status in template at startup
72092cfc 1821 connectorStatus = this.getConnectorStatus(connectorId)?.bootStatus;
56eb297e
JB
1822 } else if (this.getConnectorStatus(connectorId)?.status) {
1823 // Set previous status at startup
72092cfc 1824 connectorStatus = this.getConnectorStatus(connectorId)?.status;
5ad8570f 1825 } else {
56eb297e 1826 // Set default status
6e939d9e 1827 connectorStatus = ConnectorStatusEnum.AVAILABLE;
5ad8570f 1828 }
56eb297e
JB
1829 await this.ocppRequestService.requestHandler<
1830 StatusNotificationRequest,
1831 StatusNotificationResponse
6e939d9e
JB
1832 >(
1833 this,
1834 RequestCommand.STATUS_NOTIFICATION,
1835 OCPPServiceUtils.buildStatusNotificationRequest(this, connectorId, connectorStatus)
1836 );
1837 this.getConnectorStatus(connectorId).status = connectorStatus;
5ad8570f 1838 }
c9a4f9ea
JB
1839 if (this.stationInfo?.firmwareStatus === FirmwareStatus.Installing) {
1840 await this.ocppRequestService.requestHandler<
1841 FirmwareStatusNotificationRequest,
1842 FirmwareStatusNotificationResponse
1843 >(this, RequestCommand.FIRMWARE_STATUS_NOTIFICATION, {
1844 status: FirmwareStatus.Installed,
1845 });
1846 this.stationInfo.firmwareStatus = FirmwareStatus.Installed;
c9a4f9ea 1847 }
3637ca2c 1848
0a60c33c 1849 // Start the ATG
60ddad53 1850 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable === true) {
4f69be04 1851 this.startAutomaticTransactionGenerator();
fa7bccf4 1852 }
aa428a31 1853 this.wsConnectionRestarted === true && this.flushMessageBuffer();
fa7bccf4
JB
1854 }
1855
e7aeea18
JB
1856 private async stopMessageSequence(
1857 reason: StopTransactionReason = StopTransactionReason.NONE
1858 ): Promise<void> {
136c90ba 1859 // Stop WebSocket ping
c0560973 1860 this.stopWebSocketPing();
79411696 1861 // Stop heartbeat
c0560973 1862 this.stopHeartbeat();
fa7bccf4 1863 // Stop ongoing transactions
b20eb107 1864 if (this.automaticTransactionGenerator?.started === true) {
60ddad53
JB
1865 this.stopAutomaticTransactionGenerator();
1866 } else {
1867 await this.stopRunningTransactions(reason);
79411696 1868 }
45c0ae82
JB
1869 for (const connectorId of this.connectors.keys()) {
1870 if (connectorId > 0) {
1871 await this.ocppRequestService.requestHandler<
1872 StatusNotificationRequest,
1873 StatusNotificationResponse
6e939d9e
JB
1874 >(
1875 this,
1876 RequestCommand.STATUS_NOTIFICATION,
1877 OCPPServiceUtils.buildStatusNotificationRequest(
1878 this,
1879 connectorId,
1880 ConnectorStatusEnum.UNAVAILABLE
1881 )
1882 );
72092cfc 1883 this.getConnectorStatus(connectorId).status = undefined;
45c0ae82
JB
1884 }
1885 }
79411696
JB
1886 }
1887
c0560973 1888 private startWebSocketPing(): void {
17ac262c
JB
1889 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1890 this,
e7aeea18
JB
1891 StandardParametersKey.WebSocketPingInterval
1892 )
1893 ? Utils.convertToInt(
17ac262c
JB
1894 ChargingStationConfigurationUtils.getConfigurationKey(
1895 this,
1896 StandardParametersKey.WebSocketPingInterval
72092cfc 1897 )?.value
e7aeea18 1898 )
9cd3dfb0 1899 : 0;
ad2f27c3
JB
1900 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1901 this.webSocketPingSetInterval = setInterval(() => {
56eb297e 1902 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 1903 this.wsConnection?.ping();
136c90ba
JB
1904 }
1905 }, webSocketPingInterval * 1000);
e7aeea18 1906 logger.info(
44eb6026
JB
1907 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
1908 webSocketPingInterval
1909 )}`
e7aeea18 1910 );
ad2f27c3 1911 } else if (this.webSocketPingSetInterval) {
e7aeea18 1912 logger.info(
44eb6026
JB
1913 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
1914 webSocketPingInterval
1915 )}`
e7aeea18 1916 );
136c90ba 1917 } else {
e7aeea18
JB
1918 logger.error(
1919 `${this.logPrefix()} WebSocket ping interval set to ${
1920 webSocketPingInterval
1921 ? Utils.formatDurationSeconds(webSocketPingInterval)
1922 : webSocketPingInterval
1923 }, not starting the WebSocket ping`
1924 );
136c90ba
JB
1925 }
1926 }
1927
c0560973 1928 private stopWebSocketPing(): void {
ad2f27c3
JB
1929 if (this.webSocketPingSetInterval) {
1930 clearInterval(this.webSocketPingSetInterval);
136c90ba
JB
1931 }
1932 }
1933
1f5df42a 1934 private getConfiguredSupervisionUrl(): URL {
72092cfc 1935 const supervisionUrls = this.stationInfo?.supervisionUrls ?? Configuration.getSupervisionUrls();
53ac516c 1936 if (Utils.isNotEmptyArray(supervisionUrls)) {
2dcfe98e
JB
1937 switch (Configuration.getSupervisionUrlDistribution()) {
1938 case SupervisionUrlDistribution.ROUND_ROBIN:
c72f6634
JB
1939 // FIXME
1940 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e
JB
1941 break;
1942 case SupervisionUrlDistribution.RANDOM:
c72f6634
JB
1943 this.configuredSupervisionUrlIndex = Math.floor(
1944 Utils.secureRandom() * supervisionUrls.length
1945 );
2dcfe98e 1946 break;
c72f6634
JB
1947 case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
1948 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e
JB
1949 break;
1950 default:
e7aeea18
JB
1951 logger.error(
1952 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
c72f6634 1953 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
e7aeea18
JB
1954 }`
1955 );
c72f6634 1956 this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
2dcfe98e 1957 break;
c0560973 1958 }
c72f6634 1959 return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
c0560973 1960 }
57939a9d 1961 return new URL(supervisionUrls as string);
136c90ba
JB
1962 }
1963
c72f6634 1964 private getHeartbeatInterval(): number {
17ac262c
JB
1965 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1966 this,
1967 StandardParametersKey.HeartbeatInterval
1968 );
c0560973
JB
1969 if (HeartbeatInterval) {
1970 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1971 }
17ac262c
JB
1972 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1973 this,
1974 StandardParametersKey.HeartBeatInterval
1975 );
c0560973
JB
1976 if (HeartBeatInterval) {
1977 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
0a60c33c 1978 }
b7f9e41d 1979 this.stationInfo?.autoRegister === false &&
e7aeea18
JB
1980 logger.warn(
1981 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1982 Constants.DEFAULT_HEARTBEAT_INTERVAL
1983 }`
1984 );
47e22477 1985 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
0a60c33c
JB
1986 }
1987
c0560973 1988 private stopHeartbeat(): void {
ad2f27c3
JB
1989 if (this.heartbeatSetInterval) {
1990 clearInterval(this.heartbeatSetInterval);
7dde0b73 1991 }
5ad8570f
JB
1992 }
1993
55516218 1994 private terminateWSConnection(): void {
56eb297e 1995 if (this.isWebSocketConnectionOpened() === true) {
72092cfc 1996 this.wsConnection?.terminate();
55516218
JB
1997 this.wsConnection = null;
1998 }
1999 }
2000
dd119a6b 2001 private stopMeterValues(connectorId: number) {
734d790d 2002 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
72092cfc 2003 clearInterval(this.getConnectorStatus(connectorId)?.transactionSetInterval);
dd119a6b
JB
2004 }
2005 }
2006
c72f6634 2007 private getReconnectExponentialDelay(): boolean {
a14885a3 2008 return this.stationInfo?.reconnectExponentialDelay ?? false;
5ad8570f
JB
2009 }
2010
aa428a31 2011 private async reconnect(): Promise<void> {
7874b0b1
JB
2012 // Stop WebSocket ping
2013 this.stopWebSocketPing();
136c90ba 2014 // Stop heartbeat
c0560973 2015 this.stopHeartbeat();
5ad8570f 2016 // Stop the ATG if needed
6d9876e7 2017 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure === true) {
fa7bccf4 2018 this.stopAutomaticTransactionGenerator();
ad2f27c3 2019 }
e7aeea18
JB
2020 if (
2021 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2022 this.getAutoReconnectMaxRetries() === -1
2023 ) {
ad2f27c3 2024 this.autoReconnectRetryCount++;
e7aeea18
JB
2025 const reconnectDelay = this.getReconnectExponentialDelay()
2026 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2027 : this.getConnectionTimeout() * 1000;
1e080116
JB
2028 const reconnectDelayWithdraw = 1000;
2029 const reconnectTimeout =
2030 reconnectDelay && reconnectDelay - reconnectDelayWithdraw > 0
2031 ? reconnectDelay - reconnectDelayWithdraw
2032 : 0;
e7aeea18 2033 logger.error(
d56ea27c 2034 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
e7aeea18
JB
2035 reconnectDelay,
2036 2
2037 )}ms, timeout ${reconnectTimeout}ms`
2038 );
032d6efc 2039 await Utils.sleep(reconnectDelay);
e7aeea18 2040 logger.error(
44eb6026 2041 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
e7aeea18
JB
2042 );
2043 this.openWSConnection(
ccb1d6e9 2044 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
1e080116 2045 { closeOpened: true }
e7aeea18 2046 );
265e4266 2047 this.wsConnectionRestarted = true;
c0560973 2048 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2049 logger.error(
d56ea27c 2050 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
e7aeea18 2051 this.autoReconnectRetryCount
d56ea27c 2052 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`
e7aeea18 2053 );
5ad8570f
JB
2054 }
2055 }
2056
551e477c
JB
2057 private getAutomaticTransactionGeneratorConfigurationFromTemplate():
2058 | AutomaticTransactionGeneratorConfiguration
2059 | undefined {
2060 return this.getTemplateFromFile()?.AutomaticTransactionGenerator;
fa7bccf4
JB
2061 }
2062
a2653482
JB
2063 private initializeConnectorStatus(connectorId: number): void {
2064 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2065 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2066 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d
JB
2067 this.getConnectorStatus(connectorId).transactionStarted = false;
2068 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2069 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
0a60c33c 2070 }
7dde0b73 2071}