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