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