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