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