Apply prettier formating
[e-mobility-charging-stations-simulator.git] / src / charging-station / ChargingStation.ts
CommitLineData
b4d34251
JB
1// Partial Copyright Jerome Benoit. 2021. All Rights Reserved.
2
e7aeea18
JB
3import {
4 AvailabilityType,
5 BootNotificationRequest,
6 CachedRequest,
7 IncomingRequest,
8 IncomingRequestCommand,
9 RequestCommand,
10} from '../types/ocpp/Requests';
efa43e52 11import { BootNotificationResponse, RegistrationStatus } from '../types/ocpp/Responses';
e7aeea18
JB
12import ChargingStationConfiguration, {
13 ConfigurationKey,
14} from '../types/ChargingStationConfiguration';
15import ChargingStationTemplate, {
16 CurrentType,
17 PowerUnits,
18 Voltage,
19} from '../types/ChargingStationTemplate';
20import {
21 ConnectorPhaseRotation,
22 StandardParametersKey,
23 SupportedFeatureProfiles,
24 VendorDefaultParametersKey,
25} from '../types/ocpp/Configuration';
9ccca265 26import { MeterValueMeasurand, MeterValuePhase } from '../types/ocpp/MeterValues';
16b0d4e7 27import { WSError, WebSocketCloseEventStatusCode } from '../types/WebSocket';
58fad749 28import WebSocket, { ClientOptions, Data, OPEN } from 'ws';
3f40bc9c 29
6af9012e 30import AutomaticTransactionGenerator from './AutomaticTransactionGenerator';
c0560973
JB
31import { ChargePointStatus } from '../types/ocpp/ChargePointStatus';
32import { ChargingProfile } from '../types/ocpp/ChargingProfile';
9ac86a7e 33import ChargingStationInfo from '../types/ChargingStationInfo';
ee0f106b 34import { ChargingStationWorkerMessageEvents } from '../types/ChargingStationWorker';
15042c5f 35import { ClientRequestArgs } from 'http';
6af9012e 36import Configuration from '../utils/Configuration';
057e2042 37import { ConnectorStatus } from '../types/ConnectorStatus';
63b48f77 38import Constants from '../utils/Constants';
14763b46 39import { ErrorType } from '../types/ocpp/ErrorType';
23132a44 40import FileUtils from '../utils/FileUtils';
d1888640 41import { JsonType } from '../types/JsonType';
d2a64eb5 42import { MessageType } from '../types/ocpp/MessageType';
e7171280 43import OCPP16IncomingRequestService from './ocpp/1.6/OCPP16IncomingRequestService';
c0560973
JB
44import OCPP16RequestService from './ocpp/1.6/OCPP16RequestService';
45import OCPP16ResponseService from './ocpp/1.6/OCPP16ResponseService';
e58068fd 46import OCPPError from '../exception/OCPPError';
c0560973
JB
47import OCPPIncomingRequestService from './ocpp/OCPPIncomingRequestService';
48import OCPPRequestService from './ocpp/OCPPRequestService';
49import { OCPPVersion } from '../types/ocpp/OCPPVersion';
a6b3c6c3 50import PerformanceStatistics from '../performance/PerformanceStatistics';
057e2042 51import { SampledValueTemplate } from '../types/MeasurandPerPhaseSampledValueTemplates';
c0560973 52import { StopTransactionReason } from '../types/ocpp/Transaction';
2dcfe98e 53import { SupervisionUrlDistribution } from '../types/ConfigurationData';
57939a9d 54import { URL } from 'url';
6af9012e 55import Utils from '../utils/Utils';
3f40bc9c
JB
56import crypto from 'crypto';
57import fs from 'fs';
9f2e3130 58import logger from '../utils/Logger';
ee0f106b 59import { parentPort } from 'worker_threads';
bf1866b2 60import path from 'path';
3f40bc9c
JB
61
62export default class ChargingStation {
9f2e3130 63 public readonly id: string;
9e23580d 64 public readonly stationTemplateFile: string;
c0560973 65 public authorizedTags: string[];
6e0964c8 66 public stationInfo!: ChargingStationInfo;
9e23580d 67 public readonly connectors: Map<number, ConnectorStatus>;
6e0964c8 68 public configuration!: ChargingStationConfiguration;
6e0964c8 69 public wsConnection!: WebSocket;
9e23580d 70 public readonly requests: Map<string, CachedRequest>;
6e0964c8
JB
71 public performanceStatistics!: PerformanceStatistics;
72 public heartbeatSetInterval!: NodeJS.Timeout;
6e0964c8 73 public ocppRequestService!: OCPPRequestService;
9e23580d 74 private readonly index: number;
6e0964c8
JB
75 private bootNotificationRequest!: BootNotificationRequest;
76 private bootNotificationResponse!: BootNotificationResponse | null;
77 private connectorsConfigurationHash!: string;
a472cf2b 78 private ocppIncomingRequestService!: OCPPIncomingRequestService;
8e242273 79 private readonly messageBuffer: Set<string>;
12fc74d6 80 private wsConfiguredConnectionUrl!: URL;
265e4266 81 private wsConnectionRestarted: boolean;
a472cf2b 82 private stopped: boolean;
ad2f27c3 83 private autoReconnectRetryCount: number;
265e4266 84 private automaticTransactionGenerator!: AutomaticTransactionGenerator;
6e0964c8 85 private webSocketPingSetInterval!: NodeJS.Timeout;
6af9012e
JB
86
87 constructor(index: number, stationTemplateFile: string) {
1629a152 88 this.id = Utils.generateUUID();
ad2f27c3
JB
89 this.index = index;
90 this.stationTemplateFile = stationTemplateFile;
265e4266
JB
91 this.stopped = false;
92 this.wsConnectionRestarted = false;
ad2f27c3 93 this.autoReconnectRetryCount = 0;
9f2e3130 94 this.connectors = new Map<number, ConnectorStatus>();
32b02249 95 this.requests = new Map<string, CachedRequest>();
8e242273 96 this.messageBuffer = new Set<string>();
9f2e3130 97 this.initialize();
c0560973
JB
98 this.authorizedTags = this.getAuthorizedTags();
99 }
100
12fc74d6 101 get wsConnectionUrl(): URL {
e7aeea18
JB
102 return this.getSupervisionUrlOcppConfiguration()
103 ? new URL(
104 this.getConfigurationKey(
105 this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl
106 ).value +
107 '/' +
108 this.stationInfo.chargingStationId
109 )
110 : this.wsConfiguredConnectionUrl;
12fc74d6
JB
111 }
112
c0560973 113 public logPrefix(): string {
54b1efe0 114 return Utils.logPrefix(` ${this.stationInfo.chargingStationId} |`);
c0560973
JB
115 }
116
802cfa13
JB
117 public getBootNotificationRequest(): BootNotificationRequest {
118 return this.bootNotificationRequest;
119 }
120
f4bf2abd 121 public getRandomIdTag(): string {
c37528f1 122 const index = Math.floor(Utils.secureRandom() * this.authorizedTags.length);
c0560973
JB
123 return this.authorizedTags[index];
124 }
125
126 public hasAuthorizedTags(): boolean {
127 return !Utils.isEmptyArray(this.authorizedTags);
128 }
129
6e0964c8 130 public getEnableStatistics(): boolean | undefined {
e7aeea18
JB
131 return !Utils.isUndefined(this.stationInfo.enableStatistics)
132 ? this.stationInfo.enableStatistics
133 : true;
c0560973
JB
134 }
135
a7fc8211
JB
136 public getMayAuthorizeAtRemoteStart(): boolean | undefined {
137 return this.stationInfo.mayAuthorizeAtRemoteStart ?? true;
138 }
139
6e0964c8 140 public getNumberOfPhases(): number | undefined {
7decf1b6 141 switch (this.getCurrentOutType()) {
4c2b4904 142 case CurrentType.AC:
e7aeea18
JB
143 return !Utils.isUndefined(this.stationInfo.numberOfPhases)
144 ? this.stationInfo.numberOfPhases
145 : 3;
4c2b4904 146 case CurrentType.DC:
c0560973
JB
147 return 0;
148 }
149 }
150
d5bff457 151 public isWebSocketConnectionOpened(): boolean {
e58068fd 152 return this?.wsConnection?.readyState === OPEN;
c0560973
JB
153 }
154
672fed6e
JB
155 public getRegistrationStatus(): RegistrationStatus {
156 return this?.bootNotificationResponse?.status;
157 }
158
73c4266d
JB
159 public isInUnknownState(): boolean {
160 return Utils.isNullOrUndefined(this?.bootNotificationResponse?.status);
161 }
162
16cd35ad
JB
163 public isInPendingState(): boolean {
164 return this?.bootNotificationResponse?.status === RegistrationStatus.PENDING;
165 }
166
167 public isInAcceptedState(): boolean {
e58068fd 168 return this?.bootNotificationResponse?.status === RegistrationStatus.ACCEPTED;
c0560973
JB
169 }
170
16cd35ad
JB
171 public isInRejectedState(): boolean {
172 return this?.bootNotificationResponse?.status === RegistrationStatus.REJECTED;
173 }
174
175 public isRegistered(): boolean {
73c4266d 176 return !this.isInUnknownState() && (this.isInAcceptedState() || this.isInPendingState());
16cd35ad
JB
177 }
178
c0560973 179 public isChargingStationAvailable(): boolean {
734d790d 180 return this.getConnectorStatus(0).availability === AvailabilityType.OPERATIVE;
c0560973
JB
181 }
182
183 public isConnectorAvailable(id: number): boolean {
9f2e3130 184 return id > 0 && this.getConnectorStatus(id).availability === AvailabilityType.OPERATIVE;
c0560973
JB
185 }
186
54544ef1
JB
187 public getNumberOfConnectors(): number {
188 return this.connectors.get(0) ? this.connectors.size - 1 : this.connectors.size;
189 }
190
734d790d
JB
191 public getConnectorStatus(id: number): ConnectorStatus {
192 return this.connectors.get(id);
c0560973
JB
193 }
194
4c2b4904
JB
195 public getCurrentOutType(): CurrentType | undefined {
196 return this.stationInfo.currentOutType ?? CurrentType.AC;
c0560973
JB
197 }
198
672fed6e
JB
199 public getOcppStrictCompliance(): boolean {
200 return this.stationInfo.ocppStrictCompliance ?? false;
201 }
202
6e0964c8 203 public getVoltageOut(): number | undefined {
e7aeea18
JB
204 const errMsg = `${this.logPrefix()} Unknown ${this.getCurrentOutType()} currentOutType in template file ${
205 this.stationTemplateFile
206 }, cannot define default voltage out`;
c0560973 207 let defaultVoltageOut: number;
7decf1b6 208 switch (this.getCurrentOutType()) {
4c2b4904
JB
209 case CurrentType.AC:
210 defaultVoltageOut = Voltage.VOLTAGE_230;
c0560973 211 break;
4c2b4904
JB
212 case CurrentType.DC:
213 defaultVoltageOut = Voltage.VOLTAGE_400;
c0560973
JB
214 break;
215 default:
9f2e3130 216 logger.error(errMsg);
290d006c 217 throw new Error(errMsg);
c0560973 218 }
e7aeea18
JB
219 return !Utils.isUndefined(this.stationInfo.voltageOut)
220 ? this.stationInfo.voltageOut
221 : defaultVoltageOut;
c0560973
JB
222 }
223
6e0964c8 224 public getTransactionIdTag(transactionId: number): string | undefined {
734d790d
JB
225 for (const connectorId of this.connectors.keys()) {
226 if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) {
227 return this.getConnectorStatus(connectorId).transactionIdTag;
c0560973
JB
228 }
229 }
230 }
231
6ed92bc1
JB
232 public getOutOfOrderEndMeterValues(): boolean {
233 return this.stationInfo.outOfOrderEndMeterValues ?? false;
234 }
235
236 public getBeginEndMeterValues(): boolean {
237 return this.stationInfo.beginEndMeterValues ?? false;
238 }
239
240 public getMeteringPerTransaction(): boolean {
241 return this.stationInfo.meteringPerTransaction ?? true;
242 }
243
fd0c36fa
JB
244 public getTransactionDataMeterValues(): boolean {
245 return this.stationInfo.transactionDataMeterValues ?? false;
246 }
247
9ccca265
JB
248 public getMainVoltageMeterValues(): boolean {
249 return this.stationInfo.mainVoltageMeterValues ?? true;
250 }
251
6b10669b
JB
252 public getPhaseLineToLineVoltageMeterValues(): boolean {
253 return this.stationInfo.phaseLineToLineVoltageMeterValues ?? false;
9bd87386
JB
254 }
255
6ed92bc1
JB
256 public getEnergyActiveImportRegisterByTransactionId(transactionId: number): number | undefined {
257 if (this.getMeteringPerTransaction()) {
734d790d 258 for (const connectorId of this.connectors.keys()) {
e7aeea18
JB
259 if (
260 connectorId > 0 &&
261 this.getConnectorStatus(connectorId).transactionId === transactionId
262 ) {
734d790d 263 return this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue;
6ed92bc1
JB
264 }
265 }
266 }
734d790d
JB
267 for (const connectorId of this.connectors.keys()) {
268 if (connectorId > 0 && this.getConnectorStatus(connectorId).transactionId === transactionId) {
269 return this.getConnectorStatus(connectorId).energyActiveImportRegisterValue;
c0560973
JB
270 }
271 }
272 }
273
6ed92bc1
JB
274 public getEnergyActiveImportRegisterByConnectorId(connectorId: number): number | undefined {
275 if (this.getMeteringPerTransaction()) {
734d790d 276 return this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue;
6ed92bc1 277 }
734d790d 278 return this.getConnectorStatus(connectorId).energyActiveImportRegisterValue;
6ed92bc1
JB
279 }
280
c0560973 281 public getAuthorizeRemoteTxRequests(): boolean {
e7aeea18
JB
282 const authorizeRemoteTxRequests = this.getConfigurationKey(
283 StandardParametersKey.AuthorizeRemoteTxRequests
284 );
285 return authorizeRemoteTxRequests
286 ? Utils.convertToBoolean(authorizeRemoteTxRequests.value)
287 : false;
c0560973
JB
288 }
289
290 public getLocalAuthListEnabled(): boolean {
e7aeea18
JB
291 const localAuthListEnabled = this.getConfigurationKey(
292 StandardParametersKey.LocalAuthListEnabled
293 );
c0560973
JB
294 return localAuthListEnabled ? Utils.convertToBoolean(localAuthListEnabled.value) : false;
295 }
296
297 public restartWebSocketPing(): void {
298 // Stop WebSocket ping
299 this.stopWebSocketPing();
300 // Start WebSocket ping
301 this.startWebSocketPing();
302 }
303
e7aeea18
JB
304 public getSampledValueTemplate(
305 connectorId: number,
306 measurand: MeterValueMeasurand = MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER,
307 phase?: MeterValuePhase
308 ): SampledValueTemplate | undefined {
9ed69c71 309 const onPhaseStr = phase ? `on phase ${phase} ` : '';
9ccca265 310 if (!Constants.SUPPORTED_MEASURANDS.includes(measurand)) {
e7aeea18
JB
311 logger.warn(
312 `${this.logPrefix()} Trying to get unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
313 );
9bd87386
JB
314 return;
315 }
e7aeea18
JB
316 if (
317 measurand !== MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
318 !this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
319 measurand
320 )
321 ) {
322 logger.debug(
323 `${this.logPrefix()} Trying to get MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId} not found in '${
324 StandardParametersKey.MeterValuesSampledData
325 }' OCPP parameter`
326 );
9ccca265
JB
327 return;
328 }
e7aeea18
JB
329 const sampledValueTemplates: SampledValueTemplate[] =
330 this.getConnectorStatus(connectorId).MeterValues;
331 for (
332 let index = 0;
333 !Utils.isEmptyArray(sampledValueTemplates) && index < sampledValueTemplates.length;
334 index++
335 ) {
336 if (
337 !Constants.SUPPORTED_MEASURANDS.includes(
338 sampledValueTemplates[index]?.measurand ??
339 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
340 )
341 ) {
342 logger.warn(
343 `${this.logPrefix()} Unsupported MeterValues measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
344 );
345 } else if (
346 phase &&
347 sampledValueTemplates[index]?.phase === phase &&
348 sampledValueTemplates[index]?.measurand === measurand &&
349 this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
350 measurand
351 )
352 ) {
9ccca265 353 return sampledValueTemplates[index];
e7aeea18
JB
354 } else if (
355 !phase &&
356 !sampledValueTemplates[index].phase &&
357 sampledValueTemplates[index]?.measurand === measurand &&
358 this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData).value.includes(
359 measurand
360 )
361 ) {
9ccca265 362 return sampledValueTemplates[index];
e7aeea18
JB
363 } else if (
364 measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER &&
365 (!sampledValueTemplates[index].measurand ||
366 sampledValueTemplates[index].measurand === measurand)
367 ) {
9ccca265
JB
368 return sampledValueTemplates[index];
369 }
370 }
9bd87386 371 if (measurand === MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER) {
7d75bee1 372 const errorMsg = `${this.logPrefix()} Missing MeterValues for default measurand '${measurand}' in template on connectorId ${connectorId}`;
9f2e3130 373 logger.error(errorMsg);
de96acad 374 throw new Error(errorMsg);
9ccca265 375 }
e7aeea18
JB
376 logger.debug(
377 `${this.logPrefix()} No MeterValues for measurand '${measurand}' ${onPhaseStr}in template on connectorId ${connectorId}`
378 );
9ccca265
JB
379 }
380
e644918b
JB
381 public getAutomaticTransactionGeneratorRequireAuthorize(): boolean {
382 return this.stationInfo.AutomaticTransactionGenerator.requireAuthorize ?? true;
383 }
384
c0560973 385 public startHeartbeat(): void {
e7aeea18
JB
386 if (
387 this.getHeartbeatInterval() &&
388 this.getHeartbeatInterval() > 0 &&
389 !this.heartbeatSetInterval
390 ) {
71623267
JB
391 // eslint-disable-next-line @typescript-eslint/no-misused-promises
392 this.heartbeatSetInterval = setInterval(async (): Promise<void> => {
c0560973
JB
393 await this.ocppRequestService.sendHeartbeat();
394 }, this.getHeartbeatInterval());
e7aeea18
JB
395 logger.info(
396 this.logPrefix() +
397 ' Heartbeat started every ' +
398 Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
399 );
c0560973 400 } else if (this.heartbeatSetInterval) {
e7aeea18
JB
401 logger.info(
402 this.logPrefix() +
403 ' Heartbeat already started every ' +
404 Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
405 );
c0560973 406 } else {
e7aeea18
JB
407 logger.error(
408 `${this.logPrefix()} Heartbeat interval set to ${
409 this.getHeartbeatInterval()
410 ? Utils.formatDurationMilliSeconds(this.getHeartbeatInterval())
411 : this.getHeartbeatInterval()
412 }, not starting the heartbeat`
413 );
c0560973
JB
414 }
415 }
416
417 public restartHeartbeat(): void {
418 // Stop heartbeat
419 this.stopHeartbeat();
420 // Start heartbeat
421 this.startHeartbeat();
422 }
423
424 public startMeterValues(connectorId: number, interval: number): void {
425 if (connectorId === 0) {
e7aeea18
JB
426 logger.error(
427 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId.toString()}`
428 );
c0560973
JB
429 return;
430 }
734d790d 431 if (!this.getConnectorStatus(connectorId)) {
e7aeea18
JB
432 logger.error(
433 `${this.logPrefix()} Trying to start MeterValues on non existing connector Id ${connectorId.toString()}`
434 );
c0560973
JB
435 return;
436 }
734d790d 437 if (!this.getConnectorStatus(connectorId)?.transactionStarted) {
e7aeea18
JB
438 logger.error(
439 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction started`
440 );
c0560973 441 return;
e7aeea18
JB
442 } else if (
443 this.getConnectorStatus(connectorId)?.transactionStarted &&
444 !this.getConnectorStatus(connectorId)?.transactionId
445 ) {
446 logger.error(
447 `${this.logPrefix()} Trying to start MeterValues on connector Id ${connectorId} with no transaction id`
448 );
c0560973
JB
449 return;
450 }
451 if (interval > 0) {
71623267 452 // eslint-disable-next-line @typescript-eslint/no-misused-promises
e7aeea18
JB
453 this.getConnectorStatus(connectorId).transactionSetInterval = setInterval(
454 async (): Promise<void> => {
455 await this.ocppRequestService.sendMeterValues(
456 connectorId,
457 this.getConnectorStatus(connectorId).transactionId,
458 interval
459 );
460 },
461 interval
462 );
c0560973 463 } else {
e7aeea18
JB
464 logger.error(
465 `${this.logPrefix()} Charging station ${
466 StandardParametersKey.MeterValueSampleInterval
467 } configuration set to ${
468 interval ? Utils.formatDurationMilliSeconds(interval) : interval
469 }, not sending MeterValues`
470 );
c0560973
JB
471 }
472 }
473
474 public start(): void {
7874b0b1
JB
475 if (this.getEnableStatistics()) {
476 this.performanceStatistics.start();
477 }
c0560973
JB
478 this.openWSConnection();
479 // Monitor authorization file
480 this.startAuthorizationFileMonitoring();
481 // Monitor station template file
482 this.startStationTemplateFileMonitoring();
8bf88613 483 // Handle WebSocket message
c0560973 484 this.wsConnection.on('message', this.onMessage.bind(this));
5dc8b1b5 485 // Handle WebSocket error
c0560973 486 this.wsConnection.on('error', this.onError.bind(this));
5dc8b1b5 487 // Handle WebSocket close
c0560973 488 this.wsConnection.on('close', this.onClose.bind(this));
8bf88613 489 // Handle WebSocket open
c0560973 490 this.wsConnection.on('open', this.onOpen.bind(this));
5dc8b1b5 491 // Handle WebSocket ping
c0560973 492 this.wsConnection.on('ping', this.onPing.bind(this));
5dc8b1b5 493 // Handle WebSocket pong
c0560973 494 this.wsConnection.on('pong', this.onPong.bind(this));
e7aeea18
JB
495 parentPort.postMessage({
496 id: ChargingStationWorkerMessageEvents.STARTED,
497 data: { id: this.stationInfo.chargingStationId },
498 });
c0560973
JB
499 }
500
501 public async stop(reason: StopTransactionReason = StopTransactionReason.NONE): Promise<void> {
502 // Stop message sequence
503 await this.stopMessageSequence(reason);
734d790d
JB
504 for (const connectorId of this.connectors.keys()) {
505 if (connectorId > 0) {
e7aeea18
JB
506 await this.ocppRequestService.sendStatusNotification(
507 connectorId,
508 ChargePointStatus.UNAVAILABLE
509 );
734d790d 510 this.getConnectorStatus(connectorId).status = ChargePointStatus.UNAVAILABLE;
c0560973
JB
511 }
512 }
d5bff457 513 if (this.isWebSocketConnectionOpened()) {
c0560973
JB
514 this.wsConnection.close();
515 }
7874b0b1
JB
516 if (this.getEnableStatistics()) {
517 this.performanceStatistics.stop();
518 }
c0560973 519 this.bootNotificationResponse = null;
e7aeea18
JB
520 parentPort.postMessage({
521 id: ChargingStationWorkerMessageEvents.STOPPED,
522 data: { id: this.stationInfo.chargingStationId },
523 });
265e4266 524 this.stopped = true;
c0560973
JB
525 }
526
e7aeea18
JB
527 public getConfigurationKey(
528 key: string | StandardParametersKey,
529 caseInsensitive = false
530 ): ConfigurationKey | undefined {
7874b0b1 531 return this.configuration.configurationKey.find((configElement) => {
c0560973
JB
532 if (caseInsensitive) {
533 return configElement.key.toLowerCase() === key.toLowerCase();
534 }
535 return configElement.key === key;
536 });
c0560973
JB
537 }
538
e7aeea18
JB
539 public addConfigurationKey(
540 key: string | StandardParametersKey,
541 value: string,
542 options: { readonly?: boolean; visible?: boolean; reboot?: boolean } = {
543 readonly: false,
544 visible: true,
545 reboot: false,
546 }
547 ): void {
c0560973 548 const keyFound = this.getConfigurationKey(key);
12fc74d6
JB
549 const readonly = options.readonly;
550 const visible = options.visible;
551 const reboot = options.reboot;
c0560973
JB
552 if (!keyFound) {
553 this.configuration.configurationKey.push({
554 key,
555 readonly,
556 value,
557 visible,
558 reboot,
559 });
560 } else {
e7aeea18
JB
561 logger.error(
562 `${this.logPrefix()} Trying to add an already existing configuration key: %j`,
563 keyFound
564 );
c0560973
JB
565 }
566 }
567
568 public setConfigurationKeyValue(key: string | StandardParametersKey, value: string): void {
569 const keyFound = this.getConfigurationKey(key);
570 if (keyFound) {
571 const keyIndex = this.configuration.configurationKey.indexOf(keyFound);
572 this.configuration.configurationKey[keyIndex].value = value;
573 } else {
e7aeea18
JB
574 logger.error(
575 `${this.logPrefix()} Trying to set a value on a non existing configuration key: %j`,
576 { key, value }
577 );
c0560973
JB
578 }
579 }
580
a7fc8211
JB
581 public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
582 let cpReplaced = false;
734d790d 583 if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId).chargingProfiles)) {
e7aeea18
JB
584 this.getConnectorStatus(connectorId).chargingProfiles?.forEach(
585 (chargingProfile: ChargingProfile, index: number) => {
586 if (
587 chargingProfile.chargingProfileId === cp.chargingProfileId ||
588 (chargingProfile.stackLevel === cp.stackLevel &&
589 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
590 ) {
591 this.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
592 cpReplaced = true;
593 }
c0560973 594 }
e7aeea18 595 );
c0560973 596 }
734d790d 597 !cpReplaced && this.getConnectorStatus(connectorId).chargingProfiles?.push(cp);
c0560973
JB
598 }
599
a2653482
JB
600 public resetConnectorStatus(connectorId: number): void {
601 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
602 this.getConnectorStatus(connectorId).idTagAuthorized = false;
603 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d 604 this.getConnectorStatus(connectorId).transactionStarted = false;
a2653482 605 delete this.getConnectorStatus(connectorId).localAuthorizeIdTag;
734d790d
JB
606 delete this.getConnectorStatus(connectorId).authorizeIdTag;
607 delete this.getConnectorStatus(connectorId).transactionId;
608 delete this.getConnectorStatus(connectorId).transactionIdTag;
609 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
610 delete this.getConnectorStatus(connectorId).transactionBeginMeterValue;
dd119a6b 611 this.stopMeterValues(connectorId);
2e6f5966
JB
612 }
613
8e242273
JB
614 public bufferMessage(message: string): void {
615 this.messageBuffer.add(message);
3ba2381e
JB
616 }
617
8e242273
JB
618 private flushMessageBuffer() {
619 if (this.messageBuffer.size > 0) {
620 this.messageBuffer.forEach((message) => {
aef1b33a 621 // TODO: evaluate the need to track performance
77f00f84 622 this.wsConnection.send(message);
8e242273 623 this.messageBuffer.delete(message);
77f00f84
JB
624 });
625 }
626 }
627
1f5df42a
JB
628 private getSupervisionUrlOcppConfiguration(): boolean {
629 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
630 }
631
c0560973 632 private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
ef6076c1 633 // In case of multiple instances: add instance index to charging station id
203bc097 634 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
9ccca265 635 const idSuffix = stationTemplate.nameSuffix ?? '';
e7aeea18
JB
636 return stationTemplate.fixedName
637 ? stationTemplate.baseName
638 : stationTemplate.baseName +
639 '-' +
640 instanceIndex.toString() +
641 ('000000000' + this.index.toString()).substr(
642 ('000000000' + this.index.toString()).length - 4
643 ) +
644 idSuffix;
5ad8570f
JB
645 }
646
c0560973 647 private buildStationInfo(): ChargingStationInfo {
9ac86a7e 648 let stationTemplateFromFile: ChargingStationTemplate;
5ad8570f
JB
649 try {
650 // Load template file
ad2f27c3 651 const fileDescriptor = fs.openSync(this.stationTemplateFile, 'r');
e7aeea18
JB
652 stationTemplateFromFile = JSON.parse(
653 fs.readFileSync(fileDescriptor, 'utf8')
654 ) as ChargingStationTemplate;
5ad8570f
JB
655 fs.closeSync(fileDescriptor);
656 } catch (error) {
e7aeea18
JB
657 FileUtils.handleFileException(
658 this.logPrefix(),
659 'Template',
660 this.stationTemplateFile,
661 error as NodeJS.ErrnoException
662 );
5ad8570f 663 }
2dcfe98e
JB
664 const chargingStationId = this.getChargingStationId(stationTemplateFromFile);
665 // Deprecation template keys section
e7aeea18
JB
666 this.warnDeprecatedTemplateKey(
667 stationTemplateFromFile,
668 'supervisionUrl',
669 chargingStationId,
670 "Use 'supervisionUrls' instead"
671 );
2dcfe98e 672 this.convertDeprecatedTemplateKey(stationTemplateFromFile, 'supervisionUrl', 'supervisionUrls');
e7aeea18 673 const stationInfo: ChargingStationInfo = stationTemplateFromFile ?? ({} as ChargingStationInfo);
cd8dd457 674 stationInfo.wsOptions = stationTemplateFromFile?.wsOptions ?? {};
0a60c33c 675 if (!Utils.isEmptyArray(stationTemplateFromFile.power)) {
9ac86a7e 676 stationTemplateFromFile.power = stationTemplateFromFile.power as number[];
e7aeea18
JB
677 const powerArrayRandomIndex = Math.floor(
678 Utils.secureRandom() * stationTemplateFromFile.power.length
679 );
680 stationInfo.maxPower =
681 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
682 ? stationTemplateFromFile.power[powerArrayRandomIndex] * 1000
683 : stationTemplateFromFile.power[powerArrayRandomIndex];
5ad8570f 684 } else {
510f0fa5 685 stationTemplateFromFile.power = stationTemplateFromFile.power as number;
e7aeea18
JB
686 stationInfo.maxPower =
687 stationTemplateFromFile.powerUnit === PowerUnits.KILO_WATT
688 ? stationTemplateFromFile.power * 1000
689 : stationTemplateFromFile.power;
5ad8570f 690 }
fd0c36fa
JB
691 delete stationInfo.power;
692 delete stationInfo.powerUnit;
2dcfe98e 693 stationInfo.chargingStationId = chargingStationId;
e7aeea18
JB
694 stationInfo.resetTime = stationTemplateFromFile.resetTime
695 ? stationTemplateFromFile.resetTime * 1000
696 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
9ac86a7e 697 return stationInfo;
5ad8570f
JB
698 }
699
1f5df42a 700 private getOcppVersion(): OCPPVersion {
c0560973
JB
701 return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
702 }
703
704 private handleUnsupportedVersion(version: OCPPVersion) {
e7aeea18
JB
705 const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
706 this.stationTemplateFile
707 }`;
9f2e3130 708 logger.error(errMsg);
c0560973
JB
709 throw new Error(errMsg);
710 }
711
712 private initialize(): void {
713 this.stationInfo = this.buildStationInfo();
37486900 714 this.configuration = this.getTemplateChargingStationConfiguration();
798010fa 715 delete this.stationInfo.Configuration;
ad2f27c3
JB
716 this.bootNotificationRequest = {
717 chargePointModel: this.stationInfo.chargePointModel,
718 chargePointVendor: this.stationInfo.chargePointVendor,
e7aeea18
JB
719 ...(!Utils.isUndefined(this.stationInfo.chargeBoxSerialNumberPrefix) && {
720 chargeBoxSerialNumber: this.stationInfo.chargeBoxSerialNumberPrefix,
721 }),
722 ...(!Utils.isUndefined(this.stationInfo.firmwareVersion) && {
723 firmwareVersion: this.stationInfo.firmwareVersion,
724 }),
2e6f5966 725 };
0a60c33c 726 // Build connectors if needed
c0560973 727 const maxConnectors = this.getMaxNumberOfConnectors();
6ecb15e4 728 if (maxConnectors <= 0) {
e7aeea18
JB
729 logger.warn(
730 `${this.logPrefix()} Charging station template ${
731 this.stationTemplateFile
732 } with ${maxConnectors} connectors`
733 );
7abfea5f 734 }
c0560973 735 const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
7abfea5f 736 if (templateMaxConnectors <= 0) {
e7aeea18
JB
737 logger.warn(
738 `${this.logPrefix()} Charging station template ${
739 this.stationTemplateFile
740 } with no connector configuration`
741 );
593cf3f9 742 }
ad2f27c3 743 if (!this.stationInfo.Connectors[0]) {
e7aeea18
JB
744 logger.warn(
745 `${this.logPrefix()} Charging station template ${
746 this.stationTemplateFile
747 } with no connector Id 0 configuration`
748 );
7abfea5f
JB
749 }
750 // Sanity check
e7aeea18
JB
751 if (
752 maxConnectors >
753 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
754 !this.stationInfo.randomConnectors
755 ) {
756 logger.warn(
757 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
758 this.stationTemplateFile
759 }, forcing random connector configurations affectation`
760 );
ad2f27c3 761 this.stationInfo.randomConnectors = true;
6ecb15e4 762 }
e7aeea18
JB
763 const connectorsConfigHash = crypto
764 .createHash('sha256')
765 .update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString())
766 .digest('hex');
767 const connectorsConfigChanged =
768 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
54544ef1 769 if (this.connectors?.size === 0 || connectorsConfigChanged) {
e7aeea18 770 connectorsConfigChanged && this.connectors.clear();
ad2f27c3 771 this.connectorsConfigurationHash = connectorsConfigHash;
7abfea5f 772 // Add connector Id 0
6af9012e 773 let lastConnector = '0';
ad2f27c3 774 for (lastConnector in this.stationInfo.Connectors) {
734d790d 775 const lastConnectorId = Utils.convertToInt(lastConnector);
e7aeea18
JB
776 if (
777 lastConnectorId === 0 &&
778 this.getUseConnectorId0() &&
779 this.stationInfo.Connectors[lastConnector]
780 ) {
781 this.connectors.set(
782 lastConnectorId,
783 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[lastConnector])
784 );
734d790d
JB
785 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
786 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
787 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
418106c8 788 }
0a60c33c
JB
789 }
790 }
0a60c33c 791 // Generate all connectors
e7aeea18
JB
792 if (
793 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0
794 ) {
7abfea5f 795 for (let index = 1; index <= maxConnectors; index++) {
e7aeea18
JB
796 const randConnectorId = this.stationInfo.randomConnectors
797 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
798 : index;
799 this.connectors.set(
800 index,
801 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[randConnectorId])
802 );
734d790d
JB
803 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
804 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
805 this.getConnectorStatus(index).chargingProfiles = [];
418106c8 806 }
7abfea5f 807 }
0a60c33c
JB
808 }
809 }
d4a73fb7 810 // Avoid duplication of connectors related information
ad2f27c3 811 delete this.stationInfo.Connectors;
0a60c33c 812 // Initialize transaction attributes on connectors
734d790d
JB
813 for (const connectorId of this.connectors.keys()) {
814 if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
a2653482 815 this.initializeConnectorStatus(connectorId);
0a60c33c
JB
816 }
817 }
e7aeea18
JB
818 this.wsConfiguredConnectionUrl = new URL(
819 this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId
820 );
1f5df42a 821 switch (this.getOcppVersion()) {
c0560973 822 case OCPPVersion.VERSION_16:
e7aeea18
JB
823 this.ocppIncomingRequestService =
824 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>(this);
825 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
826 this,
827 OCPP16ResponseService.getInstance<OCPP16ResponseService>(this)
828 );
c0560973
JB
829 break;
830 default:
1f5df42a 831 this.handleUnsupportedVersion(this.getOcppVersion());
c0560973
JB
832 break;
833 }
7abfea5f 834 // OCPP parameters
1f5df42a 835 this.initOcppParameters();
47e22477
JB
836 if (this.stationInfo.autoRegister) {
837 this.bootNotificationResponse = {
838 currentTime: new Date().toISOString(),
839 interval: this.getHeartbeatInterval() / 1000,
e7aeea18 840 status: RegistrationStatus.ACCEPTED,
47e22477
JB
841 };
842 }
147d0e0f
JB
843 this.stationInfo.powerDivider = this.getPowerDivider();
844 if (this.getEnableStatistics()) {
e7aeea18
JB
845 this.performanceStatistics = PerformanceStatistics.getInstance(
846 this.id,
847 this.stationInfo.chargingStationId,
848 this.wsConnectionUrl
849 );
147d0e0f
JB
850 }
851 }
852
1f5df42a 853 private initOcppParameters(): void {
e7aeea18
JB
854 if (
855 this.getSupervisionUrlOcppConfiguration() &&
856 !this.getConfigurationKey(
857 this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl
858 )
859 ) {
860 this.addConfigurationKey(
861 VendorDefaultParametersKey.ConnectionUrl,
862 this.getConfiguredSupervisionUrl().href,
863 { reboot: true }
864 );
12fc74d6 865 }
36f6a92e 866 if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) {
e7aeea18
JB
867 this.addConfigurationKey(
868 StandardParametersKey.SupportedFeatureProfiles,
869 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.Local_Auth_List_Management},${SupportedFeatureProfiles.Smart_Charging}`
870 );
871 }
872 this.addConfigurationKey(
873 StandardParametersKey.NumberOfConnectors,
874 this.getNumberOfConnectors().toString(),
875 { readonly: true }
876 );
c0560973 877 if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
e7aeea18
JB
878 this.addConfigurationKey(
879 StandardParametersKey.MeterValuesSampledData,
880 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
881 );
7abfea5f 882 }
7e1dc878
JB
883 if (!this.getConfigurationKey(StandardParametersKey.ConnectorPhaseRotation)) {
884 const connectorPhaseRotation = [];
734d790d 885 for (const connectorId of this.connectors.keys()) {
7e1dc878 886 // AC/DC
734d790d
JB
887 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
888 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
889 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
890 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
e7aeea18 891 // AC
734d790d
JB
892 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
893 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
894 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
895 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
7e1dc878
JB
896 }
897 }
e7aeea18
JB
898 this.addConfigurationKey(
899 StandardParametersKey.ConnectorPhaseRotation,
900 connectorPhaseRotation.toString()
901 );
7e1dc878 902 }
36f6a92e
JB
903 if (!this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests)) {
904 this.addConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
905 }
e7aeea18
JB
906 if (
907 !this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled) &&
908 this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles).value.includes(
909 SupportedFeatureProfiles.Local_Auth_List_Management
910 )
911 ) {
36f6a92e
JB
912 this.addConfigurationKey(StandardParametersKey.LocalAuthListEnabled, 'false');
913 }
147d0e0f 914 if (!this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
e7aeea18
JB
915 this.addConfigurationKey(
916 StandardParametersKey.ConnectionTimeOut,
917 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
918 );
8bce55bf 919 }
7dde0b73
JB
920 }
921
c0560973 922 private async onOpen(): Promise<void> {
e7aeea18
JB
923 logger.info(
924 `${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`
925 );
672fed6e 926 if (!this.isInAcceptedState()) {
c0560973
JB
927 // Send BootNotification
928 let registrationRetryCount = 0;
929 do {
e7aeea18
JB
930 this.bootNotificationResponse = await this.ocppRequestService.sendBootNotification(
931 this.bootNotificationRequest.chargePointModel,
932 this.bootNotificationRequest.chargePointVendor,
933 this.bootNotificationRequest.chargeBoxSerialNumber,
934 this.bootNotificationRequest.firmwareVersion
935 );
672fed6e
JB
936 if (!this.isInAcceptedState()) {
937 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
e7aeea18
JB
938 await Utils.sleep(
939 this.bootNotificationResponse?.interval
940 ? this.bootNotificationResponse.interval * 1000
941 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
942 );
c0560973 943 }
e7aeea18
JB
944 } while (
945 !this.isInAcceptedState() &&
946 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
947 this.getRegistrationMaxRetries() === -1)
948 );
c7db4718 949 }
16cd35ad 950 if (this.isInAcceptedState()) {
c0560973 951 await this.startMessageSequence();
265e4266
JB
952 this.stopped && (this.stopped = false);
953 if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
caad9d6b
JB
954 this.flushMessageBuffer();
955 }
2e6f5966 956 } else {
e7aeea18
JB
957 logger.error(
958 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
959 );
2e6f5966 960 }
c0560973 961 this.autoReconnectRetryCount = 0;
265e4266 962 this.wsConnectionRestarted = false;
2e6f5966
JB
963 }
964
6c65a295 965 private async onClose(code: number, reason: string): Promise<void> {
d09085e9 966 switch (code) {
6c65a295
JB
967 // Normal close
968 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 969 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18
JB
970 logger.info(
971 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
972 code
973 )}' and reason '${reason}'`
974 );
c0560973
JB
975 this.autoReconnectRetryCount = 0;
976 break;
6c65a295
JB
977 // Abnormal close
978 default:
e7aeea18
JB
979 logger.error(
980 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
981 code
982 )}' and reason '${reason}'`
983 );
d09085e9 984 await this.reconnect(code);
c0560973
JB
985 break;
986 }
2e6f5966
JB
987 }
988
16b0d4e7 989 private async onMessage(data: Data): Promise<void> {
e7aeea18
JB
990 let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [
991 0,
992 '',
993 '' as IncomingRequestCommand,
994 {},
995 {},
996 ];
997 let responseCallback: (
998 payload: JsonType | string,
999 requestPayload: JsonType | OCPPError
1000 ) => void;
9239b49a 1001 let rejectCallback: (error: OCPPError, requestStatistic?: boolean) => void;
32b02249 1002 let requestCommandName: RequestCommand | IncomingRequestCommand;
d1888640 1003 let requestPayload: JsonType | OCPPError;
32b02249 1004 let cachedRequest: CachedRequest;
c0560973
JB
1005 let errMsg: string;
1006 try {
16b0d4e7 1007 const request = JSON.parse(data.toString()) as IncomingRequest;
47e22477
JB
1008 if (Utils.isIterable(request)) {
1009 // Parse the message
1010 [messageType, messageId, commandName, commandPayload, errorDetails] = request;
1011 } else {
e7aeea18
JB
1012 throw new OCPPError(
1013 ErrorType.PROTOCOL_ERROR,
1014 'Incoming request is not iterable',
1015 commandName
1016 );
47e22477 1017 }
c0560973
JB
1018 // Check the Type of message
1019 switch (messageType) {
1020 // Incoming Message
1021 case MessageType.CALL_MESSAGE:
1022 if (this.getEnableStatistics()) {
aef1b33a 1023 this.performanceStatistics.addRequestStatistic(commandName, messageType);
c0560973
JB
1024 }
1025 // Process the call
e7aeea18
JB
1026 await this.ocppIncomingRequestService.handleRequest(
1027 messageId,
1028 commandName,
1029 commandPayload
1030 );
c0560973
JB
1031 break;
1032 // Outcome Message
1033 case MessageType.CALL_RESULT_MESSAGE:
1034 // Respond
16b0d4e7
JB
1035 cachedRequest = this.requests.get(messageId);
1036 if (Utils.isIterable(cachedRequest)) {
32b02249 1037 [responseCallback, , , requestPayload] = cachedRequest;
c0560973 1038 } else {
e7aeea18
JB
1039 throw new OCPPError(
1040 ErrorType.PROTOCOL_ERROR,
1041 `Cached request for message id ${messageId} response is not iterable`,
1042 commandName
1043 );
c0560973
JB
1044 }
1045 if (!responseCallback) {
1046 // Error
e7aeea18
JB
1047 throw new OCPPError(
1048 ErrorType.INTERNAL_ERROR,
1049 `Response for unknown message id ${messageId}`,
1050 commandName
1051 );
c0560973 1052 }
c0560973
JB
1053 responseCallback(commandName, requestPayload);
1054 break;
1055 // Error Message
1056 case MessageType.CALL_ERROR_MESSAGE:
16b0d4e7 1057 cachedRequest = this.requests.get(messageId);
16b0d4e7 1058 if (Utils.isIterable(cachedRequest)) {
32b02249 1059 [, rejectCallback, requestCommandName] = cachedRequest;
c0560973 1060 } else {
e7aeea18
JB
1061 throw new OCPPError(
1062 ErrorType.PROTOCOL_ERROR,
1063 `Cached request for message id ${messageId} error response is not iterable`
1064 );
c0560973 1065 }
32b02249
JB
1066 if (!rejectCallback) {
1067 // Error
e7aeea18
JB
1068 throw new OCPPError(
1069 ErrorType.INTERNAL_ERROR,
1070 `Error response for unknown message id ${messageId}`,
1071 requestCommandName
1072 );
32b02249 1073 }
e7aeea18
JB
1074 rejectCallback(
1075 new OCPPError(commandName, commandPayload.toString(), requestCommandName, errorDetails)
1076 );
c0560973
JB
1077 break;
1078 // Error
1079 default:
1080 errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
9f2e3130 1081 logger.error(errMsg);
14763b46 1082 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
c0560973
JB
1083 }
1084 } catch (error) {
1085 // Log
e7aeea18
JB
1086 logger.error(
1087 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1088 this.logPrefix(),
1089 data.toString(),
1090 this.requests.get(messageId),
1091 error
1092 );
c0560973 1093 // Send error
e7aeea18
JB
1094 messageType === MessageType.CALL_MESSAGE &&
1095 (await this.ocppRequestService.sendError(messageId, error as OCPPError, commandName));
c0560973 1096 }
2328be1e
JB
1097 }
1098
c0560973 1099 private onPing(): void {
9f2e3130 1100 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
c0560973
JB
1101 }
1102
1103 private onPong(): void {
9f2e3130 1104 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
c0560973
JB
1105 }
1106
16b0d4e7 1107 private async onError(error: WSError): Promise<void> {
9f2e3130 1108 logger.error(this.logPrefix() + ' WebSocket error: %j', error);
16b0d4e7 1109 // switch (error.code) {
c0560973 1110 // case 'ECONNREFUSED':
16b0d4e7 1111 // await this.reconnect(error);
c0560973
JB
1112 // break;
1113 // }
1114 }
1115
1116 private getTemplateChargingStationConfiguration(): ChargingStationConfiguration {
e7aeea18 1117 return this.stationInfo.Configuration ?? ({} as ChargingStationConfiguration);
c0560973
JB
1118 }
1119
6e0964c8 1120 private getAuthorizationFile(): string | undefined {
e7aeea18
JB
1121 return (
1122 this.stationInfo.authorizationFile &&
1123 path.join(
1124 path.resolve(__dirname, '../'),
1125 'assets',
1126 path.basename(this.stationInfo.authorizationFile)
1127 )
1128 );
c0560973
JB
1129 }
1130
1131 private getAuthorizedTags(): string[] {
1132 let authorizedTags: string[] = [];
1133 const authorizationFile = this.getAuthorizationFile();
1134 if (authorizationFile) {
1135 try {
1136 // Load authorization file
1137 const fileDescriptor = fs.openSync(authorizationFile, 'r');
1138 authorizedTags = JSON.parse(fs.readFileSync(fileDescriptor, 'utf8')) as string[];
1139 fs.closeSync(fileDescriptor);
1140 } catch (error) {
e7aeea18
JB
1141 FileUtils.handleFileException(
1142 this.logPrefix(),
1143 'Authorization',
1144 authorizationFile,
1145 error as NodeJS.ErrnoException
1146 );
c0560973
JB
1147 }
1148 } else {
e7aeea18
JB
1149 logger.info(
1150 this.logPrefix() +
1151 ' No authorization file given in template file ' +
1152 this.stationTemplateFile
1153 );
8c4da341 1154 }
c0560973
JB
1155 return authorizedTags;
1156 }
1157
6e0964c8 1158 private getUseConnectorId0(): boolean | undefined {
e7aeea18
JB
1159 return !Utils.isUndefined(this.stationInfo.useConnectorId0)
1160 ? this.stationInfo.useConnectorId0
1161 : true;
8bce55bf
JB
1162 }
1163
c0560973 1164 private getNumberOfRunningTransactions(): number {
6ecb15e4 1165 let trxCount = 0;
734d790d
JB
1166 for (const connectorId of this.connectors.keys()) {
1167 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
6ecb15e4
JB
1168 trxCount++;
1169 }
1170 }
1171 return trxCount;
1172 }
1173
1f761b9a 1174 // 0 for disabling
6e0964c8 1175 private getConnectionTimeout(): number | undefined {
291cb255 1176 if (this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
e7aeea18
JB
1177 return (
1178 parseInt(this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut).value) ??
1179 Constants.DEFAULT_CONNECTION_TIMEOUT
1180 );
291cb255 1181 }
291cb255 1182 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1183 }
1184
1f761b9a 1185 // -1 for unlimited, 0 for disabling
6e0964c8 1186 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1187 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1188 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1189 }
1190 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1191 return Configuration.getAutoReconnectMaxRetries();
1192 }
1193 return -1;
1194 }
1195
ec977daf 1196 // 0 for disabling
6e0964c8 1197 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1198 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1199 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1200 }
1201 return -1;
1202 }
1203
c0560973
JB
1204 private getPowerDivider(): number {
1205 let powerDivider = this.getNumberOfConnectors();
ad2f27c3 1206 if (this.stationInfo.powerSharedByConnectors) {
c0560973 1207 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1208 }
1209 return powerDivider;
1210 }
1211
c0560973 1212 private getTemplateMaxNumberOfConnectors(): number {
ad2f27c3 1213 return Object.keys(this.stationInfo.Connectors).length;
7abfea5f
JB
1214 }
1215
c0560973 1216 private getMaxNumberOfConnectors(): number {
e58068fd 1217 let maxConnectors: number;
ad2f27c3
JB
1218 if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
1219 const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
6ecb15e4 1220 // Distribute evenly the number of connectors
ad2f27c3
JB
1221 maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length];
1222 } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
1223 maxConnectors = this.stationInfo.numberOfConnectors as number;
488fd3a7 1224 } else {
e7aeea18
JB
1225 maxConnectors = this.stationInfo.Connectors[0]
1226 ? this.getTemplateMaxNumberOfConnectors() - 1
1227 : this.getTemplateMaxNumberOfConnectors();
5ad8570f
JB
1228 }
1229 return maxConnectors;
2e6f5966
JB
1230 }
1231
c0560973 1232 private async startMessageSequence(): Promise<void> {
6114e6f1 1233 if (this.stationInfo.autoRegister) {
e7aeea18
JB
1234 await this.ocppRequestService.sendBootNotification(
1235 this.bootNotificationRequest.chargePointModel,
1236 this.bootNotificationRequest.chargePointVendor,
1237 this.bootNotificationRequest.chargeBoxSerialNumber,
1238 this.bootNotificationRequest.firmwareVersion
1239 );
6114e6f1 1240 }
136c90ba 1241 // Start WebSocket ping
c0560973 1242 this.startWebSocketPing();
5ad8570f 1243 // Start heartbeat
c0560973 1244 this.startHeartbeat();
0a60c33c 1245 // Initialize connectors status
734d790d
JB
1246 for (const connectorId of this.connectors.keys()) {
1247 if (connectorId === 0) {
593cf3f9 1248 continue;
e7aeea18
JB
1249 } else if (
1250 !this.stopped &&
1251 !this.getConnectorStatus(connectorId)?.status &&
1252 this.getConnectorStatus(connectorId)?.bootStatus
1253 ) {
136c90ba 1254 // Send status in template at startup
e7aeea18
JB
1255 await this.ocppRequestService.sendStatusNotification(
1256 connectorId,
1257 this.getConnectorStatus(connectorId).bootStatus
1258 );
1259 this.getConnectorStatus(connectorId).status =
1260 this.getConnectorStatus(connectorId).bootStatus;
1261 } else if (
1262 this.stopped &&
1263 this.getConnectorStatus(connectorId)?.status &&
1264 this.getConnectorStatus(connectorId)?.bootStatus
1265 ) {
136c90ba 1266 // Send status in template after reset
e7aeea18
JB
1267 await this.ocppRequestService.sendStatusNotification(
1268 connectorId,
1269 this.getConnectorStatus(connectorId).bootStatus
1270 );
1271 this.getConnectorStatus(connectorId).status =
1272 this.getConnectorStatus(connectorId).bootStatus;
734d790d 1273 } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
136c90ba 1274 // Send previous status at template reload
e7aeea18
JB
1275 await this.ocppRequestService.sendStatusNotification(
1276 connectorId,
1277 this.getConnectorStatus(connectorId).status
1278 );
5ad8570f 1279 } else {
136c90ba 1280 // Send default status
e7aeea18
JB
1281 await this.ocppRequestService.sendStatusNotification(
1282 connectorId,
1283 ChargePointStatus.AVAILABLE
1284 );
734d790d 1285 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
5ad8570f
JB
1286 }
1287 }
0a60c33c 1288 // Start the ATG
dd119a6b 1289 this.startAutomaticTransactionGenerator();
dd119a6b
JB
1290 }
1291
1292 private startAutomaticTransactionGenerator() {
ad2f27c3 1293 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
265e4266 1294 if (!this.automaticTransactionGenerator) {
73b9adec 1295 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
5ad8570f 1296 }
265e4266
JB
1297 if (!this.automaticTransactionGenerator.started) {
1298 this.automaticTransactionGenerator.start();
5ad8570f
JB
1299 }
1300 }
5ad8570f
JB
1301 }
1302
e7aeea18
JB
1303 private async stopMessageSequence(
1304 reason: StopTransactionReason = StopTransactionReason.NONE
1305 ): Promise<void> {
136c90ba 1306 // Stop WebSocket ping
c0560973 1307 this.stopWebSocketPing();
79411696 1308 // Stop heartbeat
c0560973 1309 this.stopHeartbeat();
79411696 1310 // Stop the ATG
e7aeea18
JB
1311 if (
1312 this.stationInfo.AutomaticTransactionGenerator.enable &&
1313 this.automaticTransactionGenerator?.started
1314 ) {
0045cef5 1315 this.automaticTransactionGenerator.stop();
79411696 1316 } else {
734d790d
JB
1317 for (const connectorId of this.connectors.keys()) {
1318 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1319 const transactionId = this.getConnectorStatus(connectorId).transactionId;
e7aeea18
JB
1320 await this.ocppRequestService.sendStopTransaction(
1321 transactionId,
1322 this.getEnergyActiveImportRegisterByTransactionId(transactionId),
1323 this.getTransactionIdTag(transactionId),
1324 reason
1325 );
79411696
JB
1326 }
1327 }
1328 }
1329 }
1330
c0560973 1331 private startWebSocketPing(): void {
e7aeea18
JB
1332 const webSocketPingInterval: number = this.getConfigurationKey(
1333 StandardParametersKey.WebSocketPingInterval
1334 )
1335 ? Utils.convertToInt(
1336 this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value
1337 )
9cd3dfb0 1338 : 0;
ad2f27c3
JB
1339 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1340 this.webSocketPingSetInterval = setInterval(() => {
d5bff457 1341 if (this.isWebSocketConnectionOpened()) {
e7aeea18
JB
1342 this.wsConnection.ping((): void => {
1343 /* This is intentional */
1344 });
136c90ba
JB
1345 }
1346 }, webSocketPingInterval * 1000);
e7aeea18
JB
1347 logger.info(
1348 this.logPrefix() +
1349 ' WebSocket ping started every ' +
1350 Utils.formatDurationSeconds(webSocketPingInterval)
1351 );
ad2f27c3 1352 } else if (this.webSocketPingSetInterval) {
e7aeea18
JB
1353 logger.info(
1354 this.logPrefix() +
1355 ' WebSocket ping every ' +
1356 Utils.formatDurationSeconds(webSocketPingInterval) +
1357 ' already started'
1358 );
136c90ba 1359 } else {
e7aeea18
JB
1360 logger.error(
1361 `${this.logPrefix()} WebSocket ping interval set to ${
1362 webSocketPingInterval
1363 ? Utils.formatDurationSeconds(webSocketPingInterval)
1364 : webSocketPingInterval
1365 }, not starting the WebSocket ping`
1366 );
136c90ba
JB
1367 }
1368 }
1369
c0560973 1370 private stopWebSocketPing(): void {
ad2f27c3
JB
1371 if (this.webSocketPingSetInterval) {
1372 clearInterval(this.webSocketPingSetInterval);
136c90ba
JB
1373 }
1374 }
1375
e7aeea18
JB
1376 private warnDeprecatedTemplateKey(
1377 template: ChargingStationTemplate,
1378 key: string,
1379 chargingStationId: string,
1380 logMsgToAppend = ''
1381 ): void {
2dcfe98e 1382 if (!Utils.isUndefined(template[key])) {
e7aeea18
JB
1383 const logPrefixStr = ` ${chargingStationId} |`;
1384 logger.warn(
1385 `${Utils.logPrefix(logPrefixStr)} Deprecated template key '${key}' usage in file '${
1386 this.stationTemplateFile
1387 }'${logMsgToAppend && '. ' + logMsgToAppend}`
1388 );
2dcfe98e
JB
1389 }
1390 }
1391
e7aeea18
JB
1392 private convertDeprecatedTemplateKey(
1393 template: ChargingStationTemplate,
1394 deprecatedKey: string,
1395 key: string
1396 ): void {
2dcfe98e
JB
1397 if (!Utils.isUndefined(template[deprecatedKey])) {
1398 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
1399 template[key] = template[deprecatedKey];
1400 delete template[deprecatedKey];
1401 }
1402 }
1403
1f5df42a 1404 private getConfiguredSupervisionUrl(): URL {
e7aeea18
JB
1405 const supervisionUrls = Utils.cloneObject<string | string[]>(
1406 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1407 );
c0560973 1408 if (!Utils.isEmptyArray(supervisionUrls)) {
2dcfe98e
JB
1409 let urlIndex = 0;
1410 switch (Configuration.getSupervisionUrlDistribution()) {
1411 case SupervisionUrlDistribution.ROUND_ROBIN:
1412 urlIndex = (this.index - 1) % supervisionUrls.length;
1413 break;
1414 case SupervisionUrlDistribution.RANDOM:
1415 // Get a random url
1416 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1417 break;
1418 case SupervisionUrlDistribution.SEQUENTIAL:
1419 if (this.index <= supervisionUrls.length) {
1420 urlIndex = this.index - 1;
1421 } else {
e7aeea18
JB
1422 logger.warn(
1423 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1424 );
2dcfe98e
JB
1425 }
1426 break;
1427 default:
e7aeea18
JB
1428 logger.error(
1429 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1430 SupervisionUrlDistribution.ROUND_ROBIN
1431 }`
1432 );
2dcfe98e
JB
1433 urlIndex = (this.index - 1) % supervisionUrls.length;
1434 break;
c0560973 1435 }
2dcfe98e 1436 return new URL(supervisionUrls[urlIndex]);
c0560973 1437 }
57939a9d 1438 return new URL(supervisionUrls as string);
136c90ba
JB
1439 }
1440
6e0964c8 1441 private getHeartbeatInterval(): number | undefined {
c0560973
JB
1442 const HeartbeatInterval = this.getConfigurationKey(StandardParametersKey.HeartbeatInterval);
1443 if (HeartbeatInterval) {
1444 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1445 }
1446 const HeartBeatInterval = this.getConfigurationKey(StandardParametersKey.HeartBeatInterval);
1447 if (HeartBeatInterval) {
1448 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
0a60c33c 1449 }
e7aeea18
JB
1450 !this.stationInfo.autoRegister &&
1451 logger.warn(
1452 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1453 Constants.DEFAULT_HEARTBEAT_INTERVAL
1454 }`
1455 );
47e22477 1456 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
0a60c33c
JB
1457 }
1458
c0560973 1459 private stopHeartbeat(): void {
ad2f27c3
JB
1460 if (this.heartbeatSetInterval) {
1461 clearInterval(this.heartbeatSetInterval);
7dde0b73 1462 }
5ad8570f
JB
1463 }
1464
e7aeea18
JB
1465 private openWSConnection(
1466 options: ClientOptions & ClientRequestArgs = this.stationInfo.wsOptions,
1467 forceCloseOpened = false
1468 ): void {
37486900 1469 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
e7aeea18
JB
1470 if (
1471 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
1472 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
1473 ) {
15042c5f
JB
1474 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
1475 }
d5bff457 1476 if (this.isWebSocketConnectionOpened() && forceCloseOpened) {
c0560973
JB
1477 this.wsConnection.close();
1478 }
88184022 1479 let protocol: string;
1f5df42a 1480 switch (this.getOcppVersion()) {
c0560973
JB
1481 case OCPPVersion.VERSION_16:
1482 protocol = 'ocpp' + OCPPVersion.VERSION_16;
1483 break;
1484 default:
1f5df42a 1485 this.handleUnsupportedVersion(this.getOcppVersion());
c0560973
JB
1486 break;
1487 }
1488 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
e7aeea18
JB
1489 logger.info(
1490 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString()
1491 );
136c90ba
JB
1492 }
1493
dd119a6b 1494 private stopMeterValues(connectorId: number) {
734d790d
JB
1495 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1496 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
dd119a6b
JB
1497 }
1498 }
1499
c0560973 1500 private startAuthorizationFileMonitoring(): void {
23132a44
JB
1501 const authorizationFile = this.getAuthorizationFile();
1502 if (authorizationFile) {
5ad8570f 1503 try {
3ec10737
JB
1504 fs.watch(authorizationFile, (event, filename) => {
1505 if (filename && event === 'change') {
1506 try {
e7aeea18
JB
1507 logger.debug(
1508 this.logPrefix() +
1509 ' Authorization file ' +
1510 authorizationFile +
1511 ' have changed, reload'
1512 );
3ec10737
JB
1513 // Initialize authorizedTags
1514 this.authorizedTags = this.getAuthorizedTags();
1515 } catch (error) {
9f2e3130 1516 logger.error(this.logPrefix() + ' Authorization file monitoring error: %j', error);
3ec10737 1517 }
23132a44
JB
1518 }
1519 });
5ad8570f 1520 } catch (error) {
e7aeea18
JB
1521 FileUtils.handleFileException(
1522 this.logPrefix(),
1523 'Authorization',
1524 authorizationFile,
1525 error as NodeJS.ErrnoException
1526 );
5ad8570f 1527 }
23132a44 1528 } else {
e7aeea18
JB
1529 logger.info(
1530 this.logPrefix() +
1531 ' No authorization file given in template file ' +
1532 this.stationTemplateFile +
1533 '. Not monitoring changes'
1534 );
23132a44 1535 }
5ad8570f
JB
1536 }
1537
c0560973 1538 private startStationTemplateFileMonitoring(): void {
23132a44 1539 try {
b809adf1 1540 fs.watch(this.stationTemplateFile, (event, filename): void => {
3ec10737
JB
1541 if (filename && event === 'change') {
1542 try {
e7aeea18
JB
1543 logger.debug(
1544 this.logPrefix() +
1545 ' Template file ' +
1546 this.stationTemplateFile +
1547 ' have changed, reload'
1548 );
3ec10737
JB
1549 // Initialize
1550 this.initialize();
1551 // Restart the ATG
e7aeea18
JB
1552 if (
1553 !this.stationInfo.AutomaticTransactionGenerator.enable &&
1554 this.automaticTransactionGenerator
1555 ) {
0045cef5 1556 this.automaticTransactionGenerator.stop();
3ec10737
JB
1557 }
1558 this.startAutomaticTransactionGenerator();
1559 if (this.getEnableStatistics()) {
1560 this.performanceStatistics.restart();
1561 } else {
1562 this.performanceStatistics.stop();
1563 }
1564 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
1565 } catch (error) {
e7aeea18
JB
1566 logger.error(
1567 this.logPrefix() + ' Charging station template file monitoring error: %j',
1568 error
1569 );
087a502d 1570 }
79411696 1571 }
23132a44
JB
1572 });
1573 } catch (error) {
e7aeea18
JB
1574 FileUtils.handleFileException(
1575 this.logPrefix(),
1576 'Template',
1577 this.stationTemplateFile,
1578 error as NodeJS.ErrnoException
1579 );
23132a44 1580 }
5ad8570f
JB
1581 }
1582
6e0964c8 1583 private getReconnectExponentialDelay(): boolean | undefined {
e7aeea18
JB
1584 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
1585 ? this.stationInfo.reconnectExponentialDelay
1586 : false;
5ad8570f
JB
1587 }
1588
d09085e9 1589 private async reconnect(code: number): Promise<void> {
7874b0b1
JB
1590 // Stop WebSocket ping
1591 this.stopWebSocketPing();
136c90ba 1592 // Stop heartbeat
c0560973 1593 this.stopHeartbeat();
5ad8570f 1594 // Stop the ATG if needed
e7aeea18
JB
1595 if (
1596 this.stationInfo.AutomaticTransactionGenerator.enable &&
ad2f27c3 1597 this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
e7aeea18
JB
1598 this.automaticTransactionGenerator?.started
1599 ) {
0045cef5 1600 this.automaticTransactionGenerator.stop();
ad2f27c3 1601 }
e7aeea18
JB
1602 if (
1603 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
1604 this.getAutoReconnectMaxRetries() === -1
1605 ) {
ad2f27c3 1606 this.autoReconnectRetryCount++;
e7aeea18
JB
1607 const reconnectDelay = this.getReconnectExponentialDelay()
1608 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
1609 : this.getConnectionTimeout() * 1000;
1610 const reconnectTimeout = reconnectDelay - 100 > 0 && reconnectDelay;
1611 logger.error(
1612 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
1613 reconnectDelay,
1614 2
1615 )}ms, timeout ${reconnectTimeout}ms`
1616 );
032d6efc 1617 await Utils.sleep(reconnectDelay);
e7aeea18
JB
1618 logger.error(
1619 this.logPrefix() +
1620 ' WebSocket: reconnecting try #' +
1621 this.autoReconnectRetryCount.toString()
1622 );
1623 this.openWSConnection(
1624 { ...this.stationInfo.wsOptions, handshakeTimeout: reconnectTimeout },
1625 true
1626 );
265e4266 1627 this.wsConnectionRestarted = true;
c0560973 1628 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18
JB
1629 logger.error(
1630 `${this.logPrefix()} WebSocket reconnect failure: max retries reached (${
1631 this.autoReconnectRetryCount
1632 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
1633 );
5ad8570f
JB
1634 }
1635 }
1636
a2653482
JB
1637 private initializeConnectorStatus(connectorId: number): void {
1638 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
1639 this.getConnectorStatus(connectorId).idTagAuthorized = false;
1640 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d
JB
1641 this.getConnectorStatus(connectorId).transactionStarted = false;
1642 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
1643 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
0a60c33c 1644 }
7dde0b73 1645}