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