Only compute amperage limitation when needed
[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 238 public getConnectorMaximumAvailablePower(connectorId: number): number {
d20f43b5 239 let connectorAmperageLimitationPowerLimit: number;
cc6e8ab5 240 if (this.getAmperageLimitation() < this.stationInfo.maximumAmperage) {
d20f43b5 241 const 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());
d20f43b5
JB
249 connectorAmperageLimitationPowerLimit =
250 amperageLimitationPowerLimit / this.stationInfo.powerDivider;
cc6e8ab5 251 }
ad8537a7
JB
252 const connectorMaximumPower =
253 ((this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower) /
254 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 &&
98c19a07
JB
746 ((chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1] &&
747 timestamp <
748 chargingProfile.chargingSchedule.startSchedule.getTime() +
749 chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1]
750 ?.startPeriod *
751 1000) ||
752 !chargingProfile.chargingSchedule.chargingSchedulePeriod[index + 1]);
cfa9539e
JB
753 }
754 );
755 if (!Utils.isEmptyArray(chargingSchedulePeriods)) {
756 matchingChargingProfile = chargingProfile;
757 break;
758 }
759 }
760 }
761 }
762 }
ad8537a7
JB
763 let limit: number;
764 if (!Utils.isEmptyArray(chargingSchedulePeriods)) {
765 switch (this.getCurrentOutType()) {
766 case CurrentType.AC:
767 limit =
768 matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
769 ? chargingSchedulePeriods[0].limit
770 : ACElectricUtils.powerTotal(
771 this.getNumberOfPhases(),
772 this.getVoltageOut(),
773 chargingSchedulePeriods[0].limit
774 );
775 break;
776 case CurrentType.DC:
777 limit =
778 matchingChargingProfile.chargingSchedule.chargingRateUnit === ChargingRateUnitType.WATT
779 ? chargingSchedulePeriods[0].limit
780 : DCElectricUtils.power(this.getVoltageOut(), chargingSchedulePeriods[0].limit);
cfa9539e 781 }
ad8537a7
JB
782 }
783 const connectorMaximumPower =
784 ((this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower) /
785 this.stationInfo.powerDivider;
786 if (limit > connectorMaximumPower) {
787 logger.error(
021394c6
JB
788 `${this.logPrefix()} Charging profile id ${
789 matchingChargingProfile.chargingProfileId
790 } limit is greater than connector id ${connectorId} maximum, dump charging profiles' stack: %j`,
ad8537a7
JB
791 this.getConnectorStatus(connectorId).chargingProfiles
792 );
793 limit = connectorMaximumPower;
794 }
795 return limit;
cfa9539e
JB
796 }
797
a7fc8211
JB
798 public setChargingProfile(connectorId: number, cp: ChargingProfile): void {
799 let cpReplaced = false;
734d790d 800 if (!Utils.isEmptyArray(this.getConnectorStatus(connectorId).chargingProfiles)) {
e7aeea18
JB
801 this.getConnectorStatus(connectorId).chargingProfiles?.forEach(
802 (chargingProfile: ChargingProfile, index: number) => {
803 if (
804 chargingProfile.chargingProfileId === cp.chargingProfileId ||
805 (chargingProfile.stackLevel === cp.stackLevel &&
806 chargingProfile.chargingProfilePurpose === cp.chargingProfilePurpose)
807 ) {
808 this.getConnectorStatus(connectorId).chargingProfiles[index] = cp;
809 cpReplaced = true;
810 }
c0560973 811 }
e7aeea18 812 );
c0560973 813 }
734d790d 814 !cpReplaced && this.getConnectorStatus(connectorId).chargingProfiles?.push(cp);
c0560973
JB
815 }
816
a2653482
JB
817 public resetConnectorStatus(connectorId: number): void {
818 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
819 this.getConnectorStatus(connectorId).idTagAuthorized = false;
820 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d 821 this.getConnectorStatus(connectorId).transactionStarted = false;
a2653482 822 delete this.getConnectorStatus(connectorId).localAuthorizeIdTag;
734d790d
JB
823 delete this.getConnectorStatus(connectorId).authorizeIdTag;
824 delete this.getConnectorStatus(connectorId).transactionId;
825 delete this.getConnectorStatus(connectorId).transactionIdTag;
826 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
827 delete this.getConnectorStatus(connectorId).transactionBeginMeterValue;
dd119a6b 828 this.stopMeterValues(connectorId);
2e6f5966
JB
829 }
830
8e242273
JB
831 public bufferMessage(message: string): void {
832 this.messageBuffer.add(message);
3ba2381e
JB
833 }
834
8e242273
JB
835 private flushMessageBuffer() {
836 if (this.messageBuffer.size > 0) {
837 this.messageBuffer.forEach((message) => {
aef1b33a 838 // TODO: evaluate the need to track performance
77f00f84 839 this.wsConnection.send(message);
8e242273 840 this.messageBuffer.delete(message);
77f00f84
JB
841 });
842 }
843 }
844
1f5df42a
JB
845 private getSupervisionUrlOcppConfiguration(): boolean {
846 return this.stationInfo.supervisionUrlOcppConfiguration ?? false;
12fc74d6
JB
847 }
848
e8e865ea
JB
849 private getSupervisionUrlOcppKey(): string {
850 return this.stationInfo.supervisionUrlOcppKey ?? VendorDefaultParametersKey.ConnectionUrl;
851 }
852
c0560973 853 private getChargingStationId(stationTemplate: ChargingStationTemplate): string {
ef6076c1 854 // In case of multiple instances: add instance index to charging station id
203bc097 855 const instanceIndex = process.env.CF_INSTANCE_INDEX ?? 0;
9ccca265 856 const idSuffix = stationTemplate.nameSuffix ?? '';
de1ec47b 857 const idStr = '000000000' + this.index.toString();
e7aeea18
JB
858 return stationTemplate.fixedName
859 ? stationTemplate.baseName
860 : stationTemplate.baseName +
861 '-' +
862 instanceIndex.toString() +
de1ec47b 863 idStr.substring(idStr.length - 4) +
e7aeea18 864 idSuffix;
5ad8570f
JB
865 }
866
efb85e20
JB
867 private getRandomSerialNumberSuffix(params?: {
868 randomBytesLength?: number;
869 upperCase?: boolean;
870 }): string {
871 const randomSerialNumberSuffix = crypto
872 .randomBytes(params?.randomBytesLength ?? 16)
873 .toString('hex');
874 if (params?.upperCase) {
875 return randomSerialNumberSuffix.toUpperCase();
876 }
877 return randomSerialNumberSuffix;
878 }
879
9214b603 880 private getTemplateFromFile(): ChargingStationTemplate | null {
2484ac1e 881 let template: ChargingStationTemplate = null;
5ad8570f 882 try {
42a3eee7
JB
883 const measureId = `${FileType.ChargingStationTemplate} read`;
884 const beginId = PerformanceStatistics.beginMeasure(measureId);
2484ac1e 885 template = JSON.parse(fs.readFileSync(this.templateFile, 'utf8')) as ChargingStationTemplate;
42a3eee7 886 PerformanceStatistics.endMeasure(measureId, beginId);
5ad8570f 887 } catch (error) {
e7aeea18
JB
888 FileUtils.handleFileException(
889 this.logPrefix(),
a95873d8 890 FileType.ChargingStationTemplate,
2484ac1e 891 this.templateFile,
e7aeea18
JB
892 error as NodeJS.ErrnoException
893 );
5ad8570f 894 }
2484ac1e
JB
895 return template;
896 }
897
898 private createSerialNumber(
899 stationInfo: ChargingStationInfo,
7a3a2ebb
JB
900 existingStationInfo?: ChargingStationInfo,
901 params: { randomSerialNumberUpperCase?: boolean; randomSerialNumber?: boolean } = {
902 randomSerialNumberUpperCase: true,
903 randomSerialNumber: true,
904 }
2484ac1e 905 ): void {
7a3a2ebb
JB
906 params = params ?? {};
907 params.randomSerialNumberUpperCase = params?.randomSerialNumberUpperCase ?? true;
908 params.randomSerialNumber = params?.randomSerialNumber ?? true;
909 if (existingStationInfo) {
910 existingStationInfo?.chargePointSerialNumber &&
911 (stationInfo.chargePointSerialNumber = existingStationInfo.chargePointSerialNumber);
912 existingStationInfo?.chargeBoxSerialNumber &&
913 (stationInfo.chargeBoxSerialNumber = existingStationInfo.chargeBoxSerialNumber);
914 } else {
915 const serialNumberSuffix = params?.randomSerialNumber
916 ? this.getRandomSerialNumberSuffix({ upperCase: params.randomSerialNumberUpperCase })
917 : '';
918 stationInfo.chargePointSerialNumber =
919 stationInfo?.chargePointSerialNumberPrefix &&
920 stationInfo.chargePointSerialNumberPrefix + serialNumberSuffix;
921 stationInfo.chargeBoxSerialNumber =
922 stationInfo?.chargeBoxSerialNumberPrefix &&
923 stationInfo.chargeBoxSerialNumberPrefix + serialNumberSuffix;
924 }
925 }
926
927 private getStationInfoFromTemplate(): ChargingStationInfo {
928 const stationInfo: ChargingStationInfo =
929 this.getTemplateFromFile() ?? ({} as ChargingStationInfo);
2484ac1e
JB
930 stationInfo.hash = crypto
931 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
7a3a2ebb 932 .update(JSON.stringify(stationInfo))
2484ac1e 933 .digest('hex');
7a3a2ebb 934 const chargingStationId = this.getChargingStationId(stationInfo);
2dcfe98e 935 // Deprecation template keys section
e7aeea18 936 this.warnDeprecatedTemplateKey(
7a3a2ebb 937 stationInfo,
e7aeea18
JB
938 'supervisionUrl',
939 chargingStationId,
940 "Use 'supervisionUrls' instead"
941 );
7a3a2ebb
JB
942 this.convertDeprecatedTemplateKey(stationInfo, 'supervisionUrl', 'supervisionUrls');
943 stationInfo.wsOptions = stationInfo?.wsOptions ?? {};
944 if (!Utils.isEmptyArray(stationInfo.power)) {
945 stationInfo.power = stationInfo.power as number[];
946 const powerArrayRandomIndex = Math.floor(Utils.secureRandom() * stationInfo.power.length);
cc6e8ab5 947 stationInfo.maximumPower =
7a3a2ebb
JB
948 stationInfo.powerUnit === PowerUnits.KILO_WATT
949 ? stationInfo.power[powerArrayRandomIndex] * 1000
950 : stationInfo.power[powerArrayRandomIndex];
5ad8570f 951 } else {
7a3a2ebb 952 stationInfo.power = stationInfo.power as number;
cc6e8ab5 953 stationInfo.maximumPower =
7a3a2ebb
JB
954 stationInfo.powerUnit === PowerUnits.KILO_WATT
955 ? stationInfo.power * 1000
956 : stationInfo.power;
5ad8570f 957 }
fd0c36fa
JB
958 delete stationInfo.power;
959 delete stationInfo.powerUnit;
2dcfe98e 960 stationInfo.chargingStationId = chargingStationId;
7a3a2ebb
JB
961 stationInfo.resetTime = stationInfo.resetTime
962 ? stationInfo.resetTime * 1000
e7aeea18 963 : Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
9ac86a7e 964 return stationInfo;
5ad8570f
JB
965 }
966
2484ac1e
JB
967 private getStationInfoFromFile(): ChargingStationInfo | null {
968 return this.getConfigurationFromFile()?.stationInfo ?? null;
969 }
970
971 private getStationInfo(): ChargingStationInfo {
972 const stationInfoFromTemplate: ChargingStationInfo = this.getStationInfoFromTemplate();
7a3a2ebb 973 this.hashId = this.getHashId(stationInfoFromTemplate);
2484ac1e
JB
974 this.configurationFile = path.join(
975 path.resolve(__dirname, '../'),
976 'assets',
977 'configurations',
978 this.hashId + '.json'
979 );
980 const stationInfoFromFile: ChargingStationInfo = this.getStationInfoFromFile();
981 if (stationInfoFromFile?.hash === stationInfoFromTemplate.hash) {
982 return stationInfoFromFile;
983 }
7a3a2ebb 984 this.createSerialNumber(stationInfoFromTemplate, stationInfoFromFile);
2484ac1e
JB
985 return stationInfoFromTemplate;
986 }
987
988 private saveStationInfo(): void {
989 this.saveConfiguration(Section.stationInfo);
990 }
991
1f5df42a 992 private getOcppVersion(): OCPPVersion {
c0560973
JB
993 return this.stationInfo.ocppVersion ? this.stationInfo.ocppVersion : OCPPVersion.VERSION_16;
994 }
995
e8e865ea
JB
996 private getOcppPersistentConfiguration(): boolean {
997 return this.stationInfo.ocppPersistentConfiguration ?? true;
998 }
999
c0560973 1000 private handleUnsupportedVersion(version: OCPPVersion) {
e7aeea18 1001 const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
2484ac1e 1002 this.templateFile
e7aeea18 1003 }`;
9f2e3130 1004 logger.error(errMsg);
c0560973
JB
1005 throw new Error(errMsg);
1006 }
1007
2484ac1e
JB
1008 private createBootNotificationRequest(stationInfo: ChargingStationInfo): BootNotificationRequest {
1009 return {
1010 chargePointModel: stationInfo.chargePointModel,
1011 chargePointVendor: stationInfo.chargePointVendor,
1012 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumber) && {
1013 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumber,
e7aeea18 1014 }),
2484ac1e
JB
1015 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumber) && {
1016 chargePointSerialNumber: stationInfo.chargePointSerialNumber,
43bb4cd9 1017 }),
2484ac1e
JB
1018 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
1019 firmwareVersion: stationInfo.firmwareVersion,
e7aeea18 1020 }),
2484ac1e
JB
1021 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
1022 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
1023 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
1024 meterSerialNumber: stationInfo.meterSerialNumber,
3f94cab5 1025 }),
2484ac1e
JB
1026 ...(!Utils.isUndefined(stationInfo.meterType) && {
1027 meterType: stationInfo.meterType,
3f94cab5 1028 }),
2e6f5966 1029 };
2484ac1e
JB
1030 }
1031
7a3a2ebb
JB
1032 private getHashId(stationInfo: ChargingStationInfo): string {
1033 const hashBootNotificationRequest = {
1034 chargePointModel: stationInfo.chargePointModel,
1035 chargePointVendor: stationInfo.chargePointVendor,
1036 ...(!Utils.isUndefined(stationInfo.chargeBoxSerialNumberPrefix) && {
1037 chargeBoxSerialNumber: stationInfo.chargeBoxSerialNumberPrefix,
1038 }),
1039 ...(!Utils.isUndefined(stationInfo.chargePointSerialNumberPrefix) && {
1040 chargePointSerialNumber: stationInfo.chargePointSerialNumberPrefix,
1041 }),
1042 ...(!Utils.isUndefined(stationInfo.firmwareVersion) && {
1043 firmwareVersion: stationInfo.firmwareVersion,
1044 }),
1045 ...(!Utils.isUndefined(stationInfo.iccid) && { iccid: stationInfo.iccid }),
1046 ...(!Utils.isUndefined(stationInfo.imsi) && { imsi: stationInfo.imsi }),
1047 ...(!Utils.isUndefined(stationInfo.meterSerialNumber) && {
1048 meterSerialNumber: stationInfo.meterSerialNumber,
1049 }),
1050 ...(!Utils.isUndefined(stationInfo.meterType) && {
1051 meterType: stationInfo.meterType,
1052 }),
1053 };
2484ac1e 1054 return crypto
3f94cab5 1055 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
7a3a2ebb 1056 .update(JSON.stringify(hashBootNotificationRequest) + stationInfo.chargingStationId)
3f94cab5 1057 .digest('hex');
2484ac1e
JB
1058 }
1059
1060 private initialize(): void {
1061 this.stationInfo = this.getStationInfo();
3f94cab5 1062 logger.info(`${this.logPrefix()} Charging station hashId '${this.hashId}'`);
2484ac1e
JB
1063 this.bootNotificationRequest = this.createBootNotificationRequest(this.stationInfo);
1064 this.ocppConfiguration = this.getOcppConfiguration();
3f94cab5 1065 delete this.stationInfo.Configuration;
0a60c33c 1066 // Build connectors if needed
c0560973 1067 const maxConnectors = this.getMaxNumberOfConnectors();
6ecb15e4 1068 if (maxConnectors <= 0) {
e7aeea18
JB
1069 logger.warn(
1070 `${this.logPrefix()} Charging station template ${
2484ac1e 1071 this.templateFile
e7aeea18
JB
1072 } with ${maxConnectors} connectors`
1073 );
7abfea5f 1074 }
c0560973 1075 const templateMaxConnectors = this.getTemplateMaxNumberOfConnectors();
7abfea5f 1076 if (templateMaxConnectors <= 0) {
e7aeea18
JB
1077 logger.warn(
1078 `${this.logPrefix()} Charging station template ${
2484ac1e 1079 this.templateFile
e7aeea18
JB
1080 } with no connector configuration`
1081 );
593cf3f9 1082 }
ad2f27c3 1083 if (!this.stationInfo.Connectors[0]) {
e7aeea18
JB
1084 logger.warn(
1085 `${this.logPrefix()} Charging station template ${
2484ac1e 1086 this.templateFile
e7aeea18
JB
1087 } with no connector Id 0 configuration`
1088 );
7abfea5f
JB
1089 }
1090 // Sanity check
e7aeea18
JB
1091 if (
1092 maxConnectors >
1093 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) &&
1094 !this.stationInfo.randomConnectors
1095 ) {
1096 logger.warn(
1097 `${this.logPrefix()} Number of connectors exceeds the number of connector configurations in template ${
2484ac1e 1098 this.templateFile
e7aeea18
JB
1099 }, forcing random connector configurations affectation`
1100 );
ad2f27c3 1101 this.stationInfo.randomConnectors = true;
6ecb15e4 1102 }
e7aeea18 1103 const connectorsConfigHash = crypto
3f94cab5 1104 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
e7aeea18
JB
1105 .update(JSON.stringify(this.stationInfo.Connectors) + maxConnectors.toString())
1106 .digest('hex');
1107 const connectorsConfigChanged =
1108 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
54544ef1 1109 if (this.connectors?.size === 0 || connectorsConfigChanged) {
e7aeea18 1110 connectorsConfigChanged && this.connectors.clear();
ad2f27c3 1111 this.connectorsConfigurationHash = connectorsConfigHash;
7abfea5f 1112 // Add connector Id 0
6af9012e 1113 let lastConnector = '0';
ad2f27c3 1114 for (lastConnector in this.stationInfo.Connectors) {
734d790d 1115 const lastConnectorId = Utils.convertToInt(lastConnector);
e7aeea18
JB
1116 if (
1117 lastConnectorId === 0 &&
1118 this.getUseConnectorId0() &&
1119 this.stationInfo.Connectors[lastConnector]
1120 ) {
1121 this.connectors.set(
1122 lastConnectorId,
1123 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[lastConnector])
1124 );
734d790d
JB
1125 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1126 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1127 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
418106c8 1128 }
0a60c33c
JB
1129 }
1130 }
0a60c33c 1131 // Generate all connectors
e7aeea18
JB
1132 if (
1133 (this.stationInfo.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0
1134 ) {
7abfea5f 1135 for (let index = 1; index <= maxConnectors; index++) {
e7aeea18
JB
1136 const randConnectorId = this.stationInfo.randomConnectors
1137 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1138 : index;
1139 this.connectors.set(
1140 index,
1141 Utils.cloneObject<ConnectorStatus>(this.stationInfo.Connectors[randConnectorId])
1142 );
734d790d
JB
1143 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1144 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1145 this.getConnectorStatus(index).chargingProfiles = [];
418106c8 1146 }
7abfea5f 1147 }
0a60c33c
JB
1148 }
1149 }
cc6e8ab5
JB
1150 // The connectors attribute need to be initialized
1151 this.stationInfo.maximumAmperage = this.getMaximumAmperage();
1152 this.saveStationInfo();
7a3a2ebb 1153 // Avoid duplication of connectors related information in RAM
ad2f27c3 1154 delete this.stationInfo.Connectors;
0a60c33c 1155 // Initialize transaction attributes on connectors
734d790d
JB
1156 for (const connectorId of this.connectors.keys()) {
1157 if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
a2653482 1158 this.initializeConnectorStatus(connectorId);
0a60c33c
JB
1159 }
1160 }
e7aeea18
JB
1161 this.wsConfiguredConnectionUrl = new URL(
1162 this.getConfiguredSupervisionUrl().href + '/' + this.stationInfo.chargingStationId
1163 );
2484ac1e
JB
1164 // OCPP configuration
1165 this.initializeOcppConfiguration();
1f5df42a 1166 switch (this.getOcppVersion()) {
c0560973 1167 case OCPPVersion.VERSION_16:
e7aeea18
JB
1168 this.ocppIncomingRequestService =
1169 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>(this);
1170 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
1171 this,
1172 OCPP16ResponseService.getInstance<OCPP16ResponseService>(this)
1173 );
c0560973
JB
1174 break;
1175 default:
1f5df42a 1176 this.handleUnsupportedVersion(this.getOcppVersion());
c0560973
JB
1177 break;
1178 }
47e22477
JB
1179 if (this.stationInfo.autoRegister) {
1180 this.bootNotificationResponse = {
1181 currentTime: new Date().toISOString(),
1182 interval: this.getHeartbeatInterval() / 1000,
e7aeea18 1183 status: RegistrationStatus.ACCEPTED,
47e22477
JB
1184 };
1185 }
147d0e0f
JB
1186 this.stationInfo.powerDivider = this.getPowerDivider();
1187 if (this.getEnableStatistics()) {
e7aeea18 1188 this.performanceStatistics = PerformanceStatistics.getInstance(
3f94cab5 1189 this.hashId,
e7aeea18
JB
1190 this.stationInfo.chargingStationId,
1191 this.wsConnectionUrl
1192 );
147d0e0f
JB
1193 }
1194 }
1195
2484ac1e 1196 private initializeOcppConfiguration(): void {
e7aeea18
JB
1197 if (
1198 this.getSupervisionUrlOcppConfiguration() &&
a59737e3 1199 !this.getConfigurationKey(this.getSupervisionUrlOcppKey())
e7aeea18
JB
1200 ) {
1201 this.addConfigurationKey(
a59737e3 1202 this.getSupervisionUrlOcppKey(),
e7aeea18
JB
1203 this.getConfiguredSupervisionUrl().href,
1204 { reboot: true }
1205 );
e6895390
JB
1206 } else if (
1207 !this.getSupervisionUrlOcppConfiguration() &&
1208 this.getConfigurationKey(this.getSupervisionUrlOcppKey())
1209 ) {
1210 this.deleteConfigurationKey(this.getSupervisionUrlOcppKey(), { save: false });
12fc74d6 1211 }
cc6e8ab5
JB
1212 if (
1213 this.stationInfo.amperageLimitationOcppKey &&
1214 !this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey)
1215 ) {
1216 this.addConfigurationKey(
1217 this.stationInfo.amperageLimitationOcppKey,
1218 (this.stationInfo.maximumAmperage * this.getAmperageLimitationUnitDivider()).toString()
1219 );
1220 }
36f6a92e 1221 if (!this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles)) {
e7aeea18
JB
1222 this.addConfigurationKey(
1223 StandardParametersKey.SupportedFeatureProfiles,
1224 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.Local_Auth_List_Management},${SupportedFeatureProfiles.Smart_Charging}`
1225 );
1226 }
1227 this.addConfigurationKey(
1228 StandardParametersKey.NumberOfConnectors,
1229 this.getNumberOfConnectors().toString(),
a95873d8
JB
1230 { readonly: true },
1231 { overwrite: true }
e7aeea18 1232 );
c0560973 1233 if (!this.getConfigurationKey(StandardParametersKey.MeterValuesSampledData)) {
e7aeea18
JB
1234 this.addConfigurationKey(
1235 StandardParametersKey.MeterValuesSampledData,
1236 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1237 );
7abfea5f 1238 }
7e1dc878
JB
1239 if (!this.getConfigurationKey(StandardParametersKey.ConnectorPhaseRotation)) {
1240 const connectorPhaseRotation = [];
734d790d 1241 for (const connectorId of this.connectors.keys()) {
7e1dc878 1242 // AC/DC
734d790d
JB
1243 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1244 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1245 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1246 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
e7aeea18 1247 // AC
734d790d
JB
1248 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1249 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1250 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1251 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
7e1dc878
JB
1252 }
1253 }
e7aeea18
JB
1254 this.addConfigurationKey(
1255 StandardParametersKey.ConnectorPhaseRotation,
1256 connectorPhaseRotation.toString()
1257 );
7e1dc878 1258 }
36f6a92e
JB
1259 if (!this.getConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests)) {
1260 this.addConfigurationKey(StandardParametersKey.AuthorizeRemoteTxRequests, 'true');
1261 }
e7aeea18
JB
1262 if (
1263 !this.getConfigurationKey(StandardParametersKey.LocalAuthListEnabled) &&
1264 this.getConfigurationKey(StandardParametersKey.SupportedFeatureProfiles).value.includes(
1265 SupportedFeatureProfiles.Local_Auth_List_Management
1266 )
1267 ) {
36f6a92e
JB
1268 this.addConfigurationKey(StandardParametersKey.LocalAuthListEnabled, 'false');
1269 }
147d0e0f 1270 if (!this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
e7aeea18
JB
1271 this.addConfigurationKey(
1272 StandardParametersKey.ConnectionTimeOut,
1273 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1274 );
8bce55bf 1275 }
2484ac1e 1276 this.saveOcppConfiguration();
073bd098
JB
1277 }
1278
7f7b65ca 1279 private getConfigurationFromFile(): ChargingStationConfiguration | null {
073bd098 1280 let configuration: ChargingStationConfiguration = null;
2484ac1e 1281 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
073bd098 1282 try {
42a3eee7
JB
1283 const measureId = `${FileType.ChargingStationConfiguration} read`;
1284 const beginId = PerformanceStatistics.beginMeasure(
1285 `${FileType.ChargingStationConfiguration} read`
1286 );
073bd098 1287 configuration = JSON.parse(
a95873d8 1288 fs.readFileSync(this.configurationFile, 'utf8')
073bd098 1289 ) as ChargingStationConfiguration;
42a3eee7 1290 PerformanceStatistics.endMeasure(measureId, beginId);
073bd098
JB
1291 } catch (error) {
1292 FileUtils.handleFileException(
1293 this.logPrefix(),
a95873d8 1294 FileType.ChargingStationConfiguration,
073bd098
JB
1295 this.configurationFile,
1296 error as NodeJS.ErrnoException
1297 );
1298 }
1299 }
1300 return configuration;
1301 }
1302
2484ac1e
JB
1303 private saveConfiguration(section?: Section): void {
1304 if (this.configurationFile) {
1305 try {
1306 const configurationData: ChargingStationConfiguration =
1307 this.getConfigurationFromFile() ?? {};
1308 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1309 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
073bd098 1310 }
2484ac1e
JB
1311 switch (section) {
1312 case Section.ocppConfiguration:
1313 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1314 break;
1315 case Section.stationInfo:
1316 configurationData.stationInfo = this.stationInfo;
1317 break;
1318 default:
1319 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1320 configurationData.stationInfo = this.stationInfo;
1321 break;
1322 }
42a3eee7
JB
1323 const measureId = `${FileType.ChargingStationConfiguration} write`;
1324 const beginId = PerformanceStatistics.beginMeasure(measureId);
2484ac1e
JB
1325 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1326 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1327 fs.closeSync(fileDescriptor);
42a3eee7 1328 PerformanceStatistics.endMeasure(measureId, beginId);
2484ac1e
JB
1329 } catch (error) {
1330 FileUtils.handleFileException(
1331 this.logPrefix(),
1332 FileType.ChargingStationConfiguration,
1333 this.configurationFile,
1334 error as NodeJS.ErrnoException
073bd098
JB
1335 );
1336 }
2484ac1e
JB
1337 } else {
1338 logger.error(
1339 `${this.logPrefix()} Trying to save charging station configuration to undefined file`
1340 );
073bd098
JB
1341 }
1342 }
1343
2484ac1e
JB
1344 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration {
1345 return this.getTemplateFromFile().Configuration ?? ({} as ChargingStationOcppConfiguration);
1346 }
1347
1348 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null {
1349 let configuration: ChargingStationConfiguration = null;
1350 if (this.getOcppPersistentConfiguration()) {
7a3a2ebb
JB
1351 const configurationFromFile = this.getConfigurationFromFile();
1352 configuration = configurationFromFile?.configurationKey && configurationFromFile;
073bd098 1353 }
2484ac1e 1354 configuration && delete configuration.stationInfo;
073bd098 1355 return configuration;
7dde0b73
JB
1356 }
1357
2484ac1e
JB
1358 private getOcppConfiguration(): ChargingStationOcppConfiguration {
1359 let ocppConfiguration: ChargingStationOcppConfiguration = this.getOcppConfigurationFromFile();
1360 if (!ocppConfiguration) {
1361 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1362 }
1363 return ocppConfiguration;
1364 }
1365
1366 private saveOcppConfiguration(): void {
1367 if (this.getOcppPersistentConfiguration()) {
1368 this.saveConfiguration(Section.ocppConfiguration);
1369 }
1370 }
1371
c0560973 1372 private async onOpen(): Promise<void> {
e7aeea18
JB
1373 logger.info(
1374 `${this.logPrefix()} Connected to OCPP server through ${this.wsConnectionUrl.toString()}`
1375 );
672fed6e 1376 if (!this.isInAcceptedState()) {
c0560973
JB
1377 // Send BootNotification
1378 let registrationRetryCount = 0;
1379 do {
f22266fd
JB
1380 this.bootNotificationResponse =
1381 await this.ocppRequestService.sendMessageHandler<BootNotificationResponse>(
1382 RequestCommand.BOOT_NOTIFICATION,
1383 {
1384 chargePointModel: this.bootNotificationRequest.chargePointModel,
1385 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1386 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1387 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1388 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1389 iccid: this.bootNotificationRequest.iccid,
1390 imsi: this.bootNotificationRequest.imsi,
1391 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1392 meterType: this.bootNotificationRequest.meterType,
1393 },
1394 { skipBufferingOnError: true }
1395 );
672fed6e
JB
1396 if (!this.isInAcceptedState()) {
1397 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
e7aeea18
JB
1398 await Utils.sleep(
1399 this.bootNotificationResponse?.interval
1400 ? this.bootNotificationResponse.interval * 1000
1401 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1402 );
c0560973 1403 }
e7aeea18
JB
1404 } while (
1405 !this.isInAcceptedState() &&
1406 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1407 this.getRegistrationMaxRetries() === -1)
1408 );
c7db4718 1409 }
16cd35ad 1410 if (this.isInAcceptedState()) {
c0560973 1411 await this.startMessageSequence();
265e4266
JB
1412 this.stopped && (this.stopped = false);
1413 if (this.wsConnectionRestarted && this.isWebSocketConnectionOpened()) {
caad9d6b
JB
1414 this.flushMessageBuffer();
1415 }
2e6f5966 1416 } else {
e7aeea18
JB
1417 logger.error(
1418 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1419 );
2e6f5966 1420 }
c0560973 1421 this.autoReconnectRetryCount = 0;
265e4266 1422 this.wsConnectionRestarted = false;
2e6f5966
JB
1423 }
1424
6c65a295 1425 private async onClose(code: number, reason: string): Promise<void> {
d09085e9 1426 switch (code) {
6c65a295
JB
1427 // Normal close
1428 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
c0560973 1429 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
e7aeea18
JB
1430 logger.info(
1431 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1432 code
1433 )}' and reason '${reason}'`
1434 );
c0560973
JB
1435 this.autoReconnectRetryCount = 0;
1436 break;
6c65a295
JB
1437 // Abnormal close
1438 default:
e7aeea18
JB
1439 logger.error(
1440 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1441 code
1442 )}' and reason '${reason}'`
1443 );
d09085e9 1444 await this.reconnect(code);
c0560973
JB
1445 break;
1446 }
2e6f5966
JB
1447 }
1448
16b0d4e7 1449 private async onMessage(data: Data): Promise<void> {
e7aeea18
JB
1450 let [messageType, messageId, commandName, commandPayload, errorDetails]: IncomingRequest = [
1451 0,
1452 '',
1453 '' as IncomingRequestCommand,
1454 {},
1455 {},
1456 ];
1457 let responseCallback: (
1458 payload: JsonType | string,
1459 requestPayload: JsonType | OCPPError
1460 ) => void;
9239b49a 1461 let rejectCallback: (error: OCPPError, requestStatistic?: boolean) => void;
32b02249 1462 let requestCommandName: RequestCommand | IncomingRequestCommand;
d1888640 1463 let requestPayload: JsonType | OCPPError;
32b02249 1464 let cachedRequest: CachedRequest;
c0560973
JB
1465 let errMsg: string;
1466 try {
16b0d4e7 1467 const request = JSON.parse(data.toString()) as IncomingRequest;
47e22477
JB
1468 if (Utils.isIterable(request)) {
1469 // Parse the message
1470 [messageType, messageId, commandName, commandPayload, errorDetails] = request;
1471 } else {
e7aeea18
JB
1472 throw new OCPPError(
1473 ErrorType.PROTOCOL_ERROR,
1474 'Incoming request is not iterable',
1475 commandName
1476 );
47e22477 1477 }
c0560973
JB
1478 // Check the Type of message
1479 switch (messageType) {
1480 // Incoming Message
1481 case MessageType.CALL_MESSAGE:
1482 if (this.getEnableStatistics()) {
aef1b33a 1483 this.performanceStatistics.addRequestStatistic(commandName, messageType);
c0560973
JB
1484 }
1485 // Process the call
e7aeea18
JB
1486 await this.ocppIncomingRequestService.handleRequest(
1487 messageId,
1488 commandName,
1489 commandPayload
1490 );
c0560973
JB
1491 break;
1492 // Outcome Message
1493 case MessageType.CALL_RESULT_MESSAGE:
1494 // Respond
16b0d4e7
JB
1495 cachedRequest = this.requests.get(messageId);
1496 if (Utils.isIterable(cachedRequest)) {
32b02249 1497 [responseCallback, , , requestPayload] = cachedRequest;
c0560973 1498 } else {
e7aeea18
JB
1499 throw new OCPPError(
1500 ErrorType.PROTOCOL_ERROR,
1501 `Cached request for message id ${messageId} response is not iterable`,
1502 commandName
1503 );
c0560973
JB
1504 }
1505 if (!responseCallback) {
1506 // Error
e7aeea18
JB
1507 throw new OCPPError(
1508 ErrorType.INTERNAL_ERROR,
1509 `Response for unknown message id ${messageId}`,
1510 commandName
1511 );
c0560973 1512 }
c0560973
JB
1513 responseCallback(commandName, requestPayload);
1514 break;
1515 // Error Message
1516 case MessageType.CALL_ERROR_MESSAGE:
16b0d4e7 1517 cachedRequest = this.requests.get(messageId);
16b0d4e7 1518 if (Utils.isIterable(cachedRequest)) {
32b02249 1519 [, rejectCallback, requestCommandName] = cachedRequest;
c0560973 1520 } else {
e7aeea18
JB
1521 throw new OCPPError(
1522 ErrorType.PROTOCOL_ERROR,
1523 `Cached request for message id ${messageId} error response is not iterable`
1524 );
c0560973 1525 }
32b02249
JB
1526 if (!rejectCallback) {
1527 // Error
e7aeea18
JB
1528 throw new OCPPError(
1529 ErrorType.INTERNAL_ERROR,
1530 `Error response for unknown message id ${messageId}`,
1531 requestCommandName
1532 );
32b02249 1533 }
e7aeea18
JB
1534 rejectCallback(
1535 new OCPPError(commandName, commandPayload.toString(), requestCommandName, errorDetails)
1536 );
c0560973
JB
1537 break;
1538 // Error
1539 default:
9534e74e 1540 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
c0560973 1541 errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
9f2e3130 1542 logger.error(errMsg);
14763b46 1543 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
c0560973
JB
1544 }
1545 } catch (error) {
1546 // Log
e7aeea18
JB
1547 logger.error(
1548 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1549 this.logPrefix(),
1550 data.toString(),
1551 this.requests.get(messageId),
1552 error
1553 );
c0560973 1554 // Send error
e7aeea18
JB
1555 messageType === MessageType.CALL_MESSAGE &&
1556 (await this.ocppRequestService.sendError(messageId, error as OCPPError, commandName));
c0560973 1557 }
2328be1e
JB
1558 }
1559
c0560973 1560 private onPing(): void {
9f2e3130 1561 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
c0560973
JB
1562 }
1563
1564 private onPong(): void {
9f2e3130 1565 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
c0560973
JB
1566 }
1567
9534e74e 1568 private onError(error: WSError): void {
9f2e3130 1569 logger.error(this.logPrefix() + ' WebSocket error: %j', error);
c0560973
JB
1570 }
1571
6e0964c8 1572 private getAuthorizationFile(): string | undefined {
e7aeea18
JB
1573 return (
1574 this.stationInfo.authorizationFile &&
1575 path.join(
1576 path.resolve(__dirname, '../'),
1577 'assets',
1578 path.basename(this.stationInfo.authorizationFile)
1579 )
1580 );
c0560973
JB
1581 }
1582
1583 private getAuthorizedTags(): string[] {
1584 let authorizedTags: string[] = [];
1585 const authorizationFile = this.getAuthorizationFile();
1586 if (authorizationFile) {
1587 try {
1588 // Load authorization file
a95873d8 1589 authorizedTags = JSON.parse(fs.readFileSync(authorizationFile, 'utf8')) as string[];
c0560973 1590 } catch (error) {
e7aeea18
JB
1591 FileUtils.handleFileException(
1592 this.logPrefix(),
a95873d8 1593 FileType.Authorization,
e7aeea18
JB
1594 authorizationFile,
1595 error as NodeJS.ErrnoException
1596 );
c0560973
JB
1597 }
1598 } else {
e7aeea18 1599 logger.info(
2484ac1e 1600 this.logPrefix() + ' No authorization file given in template file ' + this.templateFile
e7aeea18 1601 );
8c4da341 1602 }
c0560973
JB
1603 return authorizedTags;
1604 }
1605
6e0964c8 1606 private getUseConnectorId0(): boolean | undefined {
e7aeea18
JB
1607 return !Utils.isUndefined(this.stationInfo.useConnectorId0)
1608 ? this.stationInfo.useConnectorId0
1609 : true;
8bce55bf
JB
1610 }
1611
c0560973 1612 private getNumberOfRunningTransactions(): number {
6ecb15e4 1613 let trxCount = 0;
734d790d
JB
1614 for (const connectorId of this.connectors.keys()) {
1615 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
6ecb15e4
JB
1616 trxCount++;
1617 }
1618 }
1619 return trxCount;
1620 }
1621
1f761b9a 1622 // 0 for disabling
6e0964c8 1623 private getConnectionTimeout(): number | undefined {
291cb255 1624 if (this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut)) {
e7aeea18
JB
1625 return (
1626 parseInt(this.getConfigurationKey(StandardParametersKey.ConnectionTimeOut).value) ??
1627 Constants.DEFAULT_CONNECTION_TIMEOUT
1628 );
291cb255 1629 }
291cb255 1630 return Constants.DEFAULT_CONNECTION_TIMEOUT;
3574dfd3
JB
1631 }
1632
1f761b9a 1633 // -1 for unlimited, 0 for disabling
6e0964c8 1634 private getAutoReconnectMaxRetries(): number | undefined {
ad2f27c3
JB
1635 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1636 return this.stationInfo.autoReconnectMaxRetries;
3574dfd3
JB
1637 }
1638 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1639 return Configuration.getAutoReconnectMaxRetries();
1640 }
1641 return -1;
1642 }
1643
ec977daf 1644 // 0 for disabling
6e0964c8 1645 private getRegistrationMaxRetries(): number | undefined {
ad2f27c3
JB
1646 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1647 return this.stationInfo.registrationMaxRetries;
32a1eb7a
JB
1648 }
1649 return -1;
1650 }
1651
c0560973
JB
1652 private getPowerDivider(): number {
1653 let powerDivider = this.getNumberOfConnectors();
ad2f27c3 1654 if (this.stationInfo.powerSharedByConnectors) {
c0560973 1655 powerDivider = this.getNumberOfRunningTransactions();
6ecb15e4
JB
1656 }
1657 return powerDivider;
1658 }
1659
c0560973 1660 private getTemplateMaxNumberOfConnectors(): number {
ad2f27c3 1661 return Object.keys(this.stationInfo.Connectors).length;
7abfea5f
JB
1662 }
1663
c0560973 1664 private getMaxNumberOfConnectors(): number {
e58068fd 1665 let maxConnectors: number;
ad2f27c3
JB
1666 if (!Utils.isEmptyArray(this.stationInfo.numberOfConnectors)) {
1667 const numberOfConnectors = this.stationInfo.numberOfConnectors as number[];
6ecb15e4 1668 // Distribute evenly the number of connectors
ad2f27c3
JB
1669 maxConnectors = numberOfConnectors[(this.index - 1) % numberOfConnectors.length];
1670 } else if (!Utils.isUndefined(this.stationInfo.numberOfConnectors)) {
1671 maxConnectors = this.stationInfo.numberOfConnectors as number;
488fd3a7 1672 } else {
e7aeea18
JB
1673 maxConnectors = this.stationInfo.Connectors[0]
1674 ? this.getTemplateMaxNumberOfConnectors() - 1
1675 : this.getTemplateMaxNumberOfConnectors();
5ad8570f
JB
1676 }
1677 return maxConnectors;
2e6f5966
JB
1678 }
1679
cc6e8ab5 1680 private getMaximumAmperage(): number | undefined {
ad8537a7 1681 const maximumPower = (this.stationInfo['maxPower'] as number) ?? this.stationInfo.maximumPower;
cc6e8ab5
JB
1682 switch (this.getCurrentOutType()) {
1683 case CurrentType.AC:
1684 return ACElectricUtils.amperagePerPhaseFromPower(
1685 this.getNumberOfPhases(),
ad8537a7 1686 maximumPower / this.getNumberOfConnectors(),
cc6e8ab5
JB
1687 this.getVoltageOut()
1688 );
1689 case CurrentType.DC:
ad8537a7 1690 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut());
cc6e8ab5
JB
1691 }
1692 }
1693
1694 private getAmperageLimitationUnitDivider(): number {
1695 let unitDivider = 1;
1696 switch (this.stationInfo.amperageLimitationUnit) {
1697 case AmpereUnits.DECI_AMPERE:
1698 unitDivider = 10;
1699 break;
1700 case AmpereUnits.CENTI_AMPERE:
1701 unitDivider = 100;
1702 break;
1703 case AmpereUnits.MILLI_AMPERE:
1704 unitDivider = 1000;
1705 break;
1706 }
1707 return unitDivider;
1708 }
1709
1710 private getAmperageLimitation(): number | undefined {
1711 if (
1712 this.stationInfo.amperageLimitationOcppKey &&
1713 this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey)
1714 ) {
1715 return (
1716 Utils.convertToInt(
1717 this.getConfigurationKey(this.stationInfo.amperageLimitationOcppKey).value
1718 ) / this.getAmperageLimitationUnitDivider()
1719 );
1720 }
1721 }
1722
c0560973 1723 private async startMessageSequence(): Promise<void> {
6114e6f1 1724 if (this.stationInfo.autoRegister) {
f22266fd 1725 await this.ocppRequestService.sendMessageHandler<BootNotificationResponse>(
6a8b180d
JB
1726 RequestCommand.BOOT_NOTIFICATION,
1727 {
1728 chargePointModel: this.bootNotificationRequest.chargePointModel,
1729 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1730 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1731 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
29d1e2e7
JB
1732 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1733 iccid: this.bootNotificationRequest.iccid,
1734 imsi: this.bootNotificationRequest.imsi,
1735 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1736 meterType: this.bootNotificationRequest.meterType,
6a8b180d
JB
1737 },
1738 { skipBufferingOnError: true }
e7aeea18 1739 );
6114e6f1 1740 }
136c90ba 1741 // Start WebSocket ping
c0560973 1742 this.startWebSocketPing();
5ad8570f 1743 // Start heartbeat
c0560973 1744 this.startHeartbeat();
0a60c33c 1745 // Initialize connectors status
734d790d
JB
1746 for (const connectorId of this.connectors.keys()) {
1747 if (connectorId === 0) {
593cf3f9 1748 continue;
e7aeea18
JB
1749 } else if (
1750 !this.stopped &&
1751 !this.getConnectorStatus(connectorId)?.status &&
1752 this.getConnectorStatus(connectorId)?.bootStatus
1753 ) {
136c90ba 1754 // Send status in template at startup
f22266fd
JB
1755 await this.ocppRequestService.sendMessageHandler<StatusNotificationResponse>(
1756 RequestCommand.STATUS_NOTIFICATION,
1757 {
1758 connectorId,
1759 status: this.getConnectorStatus(connectorId).bootStatus,
1760 errorCode: ChargePointErrorCode.NO_ERROR,
1761 }
1762 );
e7aeea18
JB
1763 this.getConnectorStatus(connectorId).status =
1764 this.getConnectorStatus(connectorId).bootStatus;
1765 } else if (
1766 this.stopped &&
1767 this.getConnectorStatus(connectorId)?.status &&
1768 this.getConnectorStatus(connectorId)?.bootStatus
1769 ) {
136c90ba 1770 // Send status in template after reset
f22266fd
JB
1771 await this.ocppRequestService.sendMessageHandler<StatusNotificationResponse>(
1772 RequestCommand.STATUS_NOTIFICATION,
1773 {
1774 connectorId,
1775 status: this.getConnectorStatus(connectorId).bootStatus,
1776 errorCode: ChargePointErrorCode.NO_ERROR,
1777 }
1778 );
e7aeea18
JB
1779 this.getConnectorStatus(connectorId).status =
1780 this.getConnectorStatus(connectorId).bootStatus;
734d790d 1781 } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
136c90ba 1782 // Send previous status at template reload
f22266fd
JB
1783 await this.ocppRequestService.sendMessageHandler<StatusNotificationResponse>(
1784 RequestCommand.STATUS_NOTIFICATION,
1785 {
1786 connectorId,
1787 status: this.getConnectorStatus(connectorId).status,
1788 errorCode: ChargePointErrorCode.NO_ERROR,
1789 }
1790 );
5ad8570f 1791 } else {
136c90ba 1792 // Send default status
f22266fd
JB
1793 await this.ocppRequestService.sendMessageHandler<StatusNotificationResponse>(
1794 RequestCommand.STATUS_NOTIFICATION,
1795 {
1796 connectorId,
1797 status: ChargePointStatus.AVAILABLE,
1798 errorCode: ChargePointErrorCode.NO_ERROR,
1799 }
1800 );
734d790d 1801 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
5ad8570f
JB
1802 }
1803 }
0a60c33c 1804 // Start the ATG
dd119a6b 1805 this.startAutomaticTransactionGenerator();
dd119a6b
JB
1806 }
1807
1808 private startAutomaticTransactionGenerator() {
ad2f27c3 1809 if (this.stationInfo.AutomaticTransactionGenerator.enable) {
265e4266 1810 if (!this.automaticTransactionGenerator) {
73b9adec 1811 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(this);
5ad8570f 1812 }
265e4266
JB
1813 if (!this.automaticTransactionGenerator.started) {
1814 this.automaticTransactionGenerator.start();
5ad8570f
JB
1815 }
1816 }
5ad8570f
JB
1817 }
1818
e7aeea18
JB
1819 private async stopMessageSequence(
1820 reason: StopTransactionReason = StopTransactionReason.NONE
1821 ): Promise<void> {
136c90ba 1822 // Stop WebSocket ping
c0560973 1823 this.stopWebSocketPing();
79411696 1824 // Stop heartbeat
c0560973 1825 this.stopHeartbeat();
79411696 1826 // Stop the ATG
e7aeea18
JB
1827 if (
1828 this.stationInfo.AutomaticTransactionGenerator.enable &&
1829 this.automaticTransactionGenerator?.started
1830 ) {
0045cef5 1831 this.automaticTransactionGenerator.stop();
79411696 1832 } else {
734d790d
JB
1833 for (const connectorId of this.connectors.keys()) {
1834 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1835 const transactionId = this.getConnectorStatus(connectorId).transactionId;
68c993d5
JB
1836 if (
1837 this.getBeginEndMeterValues() &&
1838 this.getOcppStrictCompliance() &&
1839 !this.getOutOfOrderEndMeterValues()
1840 ) {
1841 // FIXME: Implement OCPP version agnostic helpers
1842 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
1843 this,
1844 connectorId,
1845 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
1846 );
f22266fd
JB
1847 await this.ocppRequestService.sendMessageHandler<MeterValuesResponse>(
1848 RequestCommand.METER_VALUES,
1849 {
1850 connectorId,
1851 transactionId,
1852 meterValue: transactionEndMeterValue,
1853 }
1854 );
68c993d5 1855 }
f22266fd
JB
1856 await this.ocppRequestService.sendMessageHandler<StopTransactionResponse>(
1857 RequestCommand.STOP_TRANSACTION,
1858 {
1859 transactionId,
1860 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId),
1861 idTag: this.getTransactionIdTag(transactionId),
1862 reason,
1863 }
1864 );
79411696
JB
1865 }
1866 }
1867 }
1868 }
1869
c0560973 1870 private startWebSocketPing(): void {
e7aeea18
JB
1871 const webSocketPingInterval: number = this.getConfigurationKey(
1872 StandardParametersKey.WebSocketPingInterval
1873 )
1874 ? Utils.convertToInt(
1875 this.getConfigurationKey(StandardParametersKey.WebSocketPingInterval).value
1876 )
9cd3dfb0 1877 : 0;
ad2f27c3
JB
1878 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1879 this.webSocketPingSetInterval = setInterval(() => {
d5bff457 1880 if (this.isWebSocketConnectionOpened()) {
e7aeea18
JB
1881 this.wsConnection.ping((): void => {
1882 /* This is intentional */
1883 });
136c90ba
JB
1884 }
1885 }, webSocketPingInterval * 1000);
e7aeea18
JB
1886 logger.info(
1887 this.logPrefix() +
1888 ' WebSocket ping started every ' +
1889 Utils.formatDurationSeconds(webSocketPingInterval)
1890 );
ad2f27c3 1891 } else if (this.webSocketPingSetInterval) {
e7aeea18
JB
1892 logger.info(
1893 this.logPrefix() +
1894 ' WebSocket ping every ' +
1895 Utils.formatDurationSeconds(webSocketPingInterval) +
1896 ' already started'
1897 );
136c90ba 1898 } else {
e7aeea18
JB
1899 logger.error(
1900 `${this.logPrefix()} WebSocket ping interval set to ${
1901 webSocketPingInterval
1902 ? Utils.formatDurationSeconds(webSocketPingInterval)
1903 : webSocketPingInterval
1904 }, not starting the WebSocket ping`
1905 );
136c90ba
JB
1906 }
1907 }
1908
c0560973 1909 private stopWebSocketPing(): void {
ad2f27c3
JB
1910 if (this.webSocketPingSetInterval) {
1911 clearInterval(this.webSocketPingSetInterval);
136c90ba
JB
1912 }
1913 }
1914
e7aeea18
JB
1915 private warnDeprecatedTemplateKey(
1916 template: ChargingStationTemplate,
1917 key: string,
1918 chargingStationId: string,
1919 logMsgToAppend = ''
1920 ): void {
2dcfe98e 1921 if (!Utils.isUndefined(template[key])) {
e7aeea18
JB
1922 const logPrefixStr = ` ${chargingStationId} |`;
1923 logger.warn(
1924 `${Utils.logPrefix(logPrefixStr)} Deprecated template key '${key}' usage in file '${
2484ac1e 1925 this.templateFile
e7aeea18
JB
1926 }'${logMsgToAppend && '. ' + logMsgToAppend}`
1927 );
2dcfe98e
JB
1928 }
1929 }
1930
e7aeea18
JB
1931 private convertDeprecatedTemplateKey(
1932 template: ChargingStationTemplate,
1933 deprecatedKey: string,
1934 key: string
1935 ): void {
2dcfe98e 1936 if (!Utils.isUndefined(template[deprecatedKey])) {
c0f4be74 1937 template[key] = template[deprecatedKey] as unknown;
2dcfe98e
JB
1938 delete template[deprecatedKey];
1939 }
1940 }
1941
1f5df42a 1942 private getConfiguredSupervisionUrl(): URL {
e7aeea18
JB
1943 const supervisionUrls = Utils.cloneObject<string | string[]>(
1944 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1945 );
c0560973 1946 if (!Utils.isEmptyArray(supervisionUrls)) {
2dcfe98e
JB
1947 let urlIndex = 0;
1948 switch (Configuration.getSupervisionUrlDistribution()) {
1949 case SupervisionUrlDistribution.ROUND_ROBIN:
1950 urlIndex = (this.index - 1) % supervisionUrls.length;
1951 break;
1952 case SupervisionUrlDistribution.RANDOM:
1953 // Get a random url
1954 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1955 break;
1956 case SupervisionUrlDistribution.SEQUENTIAL:
1957 if (this.index <= supervisionUrls.length) {
1958 urlIndex = this.index - 1;
1959 } else {
e7aeea18
JB
1960 logger.warn(
1961 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1962 );
2dcfe98e
JB
1963 }
1964 break;
1965 default:
e7aeea18
JB
1966 logger.error(
1967 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1968 SupervisionUrlDistribution.ROUND_ROBIN
1969 }`
1970 );
2dcfe98e
JB
1971 urlIndex = (this.index - 1) % supervisionUrls.length;
1972 break;
c0560973 1973 }
2dcfe98e 1974 return new URL(supervisionUrls[urlIndex]);
c0560973 1975 }
57939a9d 1976 return new URL(supervisionUrls as string);
136c90ba
JB
1977 }
1978
6e0964c8 1979 private getHeartbeatInterval(): number | undefined {
c0560973
JB
1980 const HeartbeatInterval = this.getConfigurationKey(StandardParametersKey.HeartbeatInterval);
1981 if (HeartbeatInterval) {
1982 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1983 }
1984 const HeartBeatInterval = this.getConfigurationKey(StandardParametersKey.HeartBeatInterval);
1985 if (HeartBeatInterval) {
1986 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
0a60c33c 1987 }
e7aeea18
JB
1988 !this.stationInfo.autoRegister &&
1989 logger.warn(
1990 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1991 Constants.DEFAULT_HEARTBEAT_INTERVAL
1992 }`
1993 );
47e22477 1994 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
0a60c33c
JB
1995 }
1996
c0560973 1997 private stopHeartbeat(): void {
ad2f27c3
JB
1998 if (this.heartbeatSetInterval) {
1999 clearInterval(this.heartbeatSetInterval);
7dde0b73 2000 }
5ad8570f
JB
2001 }
2002
e7aeea18 2003 private openWSConnection(
2484ac1e 2004 options: WsOptions = this.stationInfo.wsOptions,
e7aeea18
JB
2005 forceCloseOpened = false
2006 ): void {
37486900 2007 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
e7aeea18
JB
2008 if (
2009 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
2010 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
2011 ) {
15042c5f
JB
2012 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
2013 }
d5bff457 2014 if (this.isWebSocketConnectionOpened() && forceCloseOpened) {
c0560973
JB
2015 this.wsConnection.close();
2016 }
88184022 2017 let protocol: string;
1f5df42a 2018 switch (this.getOcppVersion()) {
c0560973
JB
2019 case OCPPVersion.VERSION_16:
2020 protocol = 'ocpp' + OCPPVersion.VERSION_16;
2021 break;
2022 default:
1f5df42a 2023 this.handleUnsupportedVersion(this.getOcppVersion());
c0560973
JB
2024 break;
2025 }
2026 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
e7aeea18
JB
2027 logger.info(
2028 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString()
2029 );
136c90ba
JB
2030 }
2031
dd119a6b 2032 private stopMeterValues(connectorId: number) {
734d790d
JB
2033 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
2034 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
dd119a6b
JB
2035 }
2036 }
2037
6e0964c8 2038 private getReconnectExponentialDelay(): boolean | undefined {
e7aeea18
JB
2039 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
2040 ? this.stationInfo.reconnectExponentialDelay
2041 : false;
5ad8570f
JB
2042 }
2043
d09085e9 2044 private async reconnect(code: number): Promise<void> {
7874b0b1
JB
2045 // Stop WebSocket ping
2046 this.stopWebSocketPing();
136c90ba 2047 // Stop heartbeat
c0560973 2048 this.stopHeartbeat();
5ad8570f 2049 // Stop the ATG if needed
e7aeea18
JB
2050 if (
2051 this.stationInfo.AutomaticTransactionGenerator.enable &&
ad2f27c3 2052 this.stationInfo.AutomaticTransactionGenerator.stopOnConnectionFailure &&
e7aeea18
JB
2053 this.automaticTransactionGenerator?.started
2054 ) {
0045cef5 2055 this.automaticTransactionGenerator.stop();
ad2f27c3 2056 }
e7aeea18
JB
2057 if (
2058 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
2059 this.getAutoReconnectMaxRetries() === -1
2060 ) {
ad2f27c3 2061 this.autoReconnectRetryCount++;
e7aeea18
JB
2062 const reconnectDelay = this.getReconnectExponentialDelay()
2063 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
2064 : this.getConnectionTimeout() * 1000;
2065 const reconnectTimeout = reconnectDelay - 100 > 0 && reconnectDelay;
2066 logger.error(
2067 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
2068 reconnectDelay,
2069 2
2070 )}ms, timeout ${reconnectTimeout}ms`
2071 );
032d6efc 2072 await Utils.sleep(reconnectDelay);
e7aeea18
JB
2073 logger.error(
2074 this.logPrefix() +
2075 ' WebSocket: reconnecting try #' +
2076 this.autoReconnectRetryCount.toString()
2077 );
2078 this.openWSConnection(
2079 { ...this.stationInfo.wsOptions, handshakeTimeout: reconnectTimeout },
2080 true
2081 );
265e4266 2082 this.wsConnectionRestarted = true;
c0560973 2083 } else if (this.getAutoReconnectMaxRetries() !== -1) {
e7aeea18 2084 logger.error(
71a77ac2 2085 `${this.logPrefix()} WebSocket reconnect failure: maximum retries reached (${
e7aeea18
JB
2086 this.autoReconnectRetryCount
2087 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
2088 );
5ad8570f
JB
2089 }
2090 }
2091
a2653482
JB
2092 private initializeConnectorStatus(connectorId: number): void {
2093 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
2094 this.getConnectorStatus(connectorId).idTagAuthorized = false;
2095 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
734d790d
JB
2096 this.getConnectorStatus(connectorId).transactionStarted = false;
2097 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
2098 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
0a60c33c 2099 }
7dde0b73 2100}