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