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