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