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