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