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