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