Fix serial number propagation at charging station template change
[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 stationInfoFromFile &&
830 ChargingStationUtils.propagateSerialNumber(
831 this.getTemplateFromFile(),
832 stationInfoFromFile,
833 stationInfoFromTemplate
834 );
835 return stationInfoFromTemplate;
836 }
837
838 private saveStationInfo(): void {
839 if (this.getStationInfoPersistentConfiguration()) {
840 this.saveConfiguration(Section.stationInfo);
841 }
842 }
843
844 private getOcppVersion(): OCPPVersion {
845 return this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16;
846 }
847
848 private getOcppPersistentConfiguration(): boolean {
849 return this.stationInfo?.ocppPersistentConfiguration ?? true;
850 }
851
852 private getStationInfoPersistentConfiguration(): boolean {
853 return this.stationInfo?.stationInfoPersistentConfiguration ?? true;
854 }
855
856 private handleUnsupportedVersion(version: OCPPVersion) {
857 const errMsg = `${this.logPrefix()} Unsupported protocol version '${version}' configured in template file ${
858 this.templateFile
859 }`;
860 logger.error(errMsg);
861 throw new Error(errMsg);
862 }
863
864 private initialize(): void {
865 this.hashId = ChargingStationUtils.getHashId(this.index, this.getTemplateFromFile());
866 logger.info(`${this.logPrefix()} Charging station hashId '${this.hashId}'`);
867 this.configurationFile = path.join(
868 path.resolve(__dirname, '../'),
869 'assets',
870 'configurations',
871 this.hashId + '.json'
872 );
873 this.stationInfo = this.getStationInfo();
874 this.saveStationInfo();
875 // Avoid duplication of connectors related information in RAM
876 this.stationInfo?.Connectors && delete this.stationInfo.Connectors;
877 this.configuredSupervisionUrl = this.getConfiguredSupervisionUrl();
878 if (this.getEnableStatistics()) {
879 this.performanceStatistics = PerformanceStatistics.getInstance(
880 this.hashId,
881 this.stationInfo.chargingStationId,
882 this.configuredSupervisionUrl
883 );
884 }
885 this.bootNotificationRequest = ChargingStationUtils.createBootNotificationRequest(
886 this.stationInfo
887 );
888 this.authorizedTags = ChargingStationUtils.getAuthorizedTags(
889 this.stationInfo,
890 this.templateFile,
891 this.logPrefix()
892 );
893 this.powerDivider = this.getPowerDivider();
894 // OCPP configuration
895 this.ocppConfiguration = this.getOcppConfiguration();
896 this.initializeOcppConfiguration();
897 switch (this.getOcppVersion()) {
898 case OCPPVersion.VERSION_16:
899 this.ocppIncomingRequestService =
900 OCPP16IncomingRequestService.getInstance<OCPP16IncomingRequestService>();
901 this.ocppRequestService = OCPP16RequestService.getInstance<OCPP16RequestService>(
902 OCPP16ResponseService.getInstance<OCPP16ResponseService>()
903 );
904 break;
905 default:
906 this.handleUnsupportedVersion(this.getOcppVersion());
907 break;
908 }
909 if (this.stationInfo.autoRegister) {
910 this.bootNotificationResponse = {
911 currentTime: new Date().toISOString(),
912 interval: this.getHeartbeatInterval() / 1000,
913 status: RegistrationStatus.ACCEPTED,
914 };
915 }
916 }
917
918 private initializeOcppConfiguration(): void {
919 if (
920 !ChargingStationConfigurationUtils.getConfigurationKey(
921 this,
922 StandardParametersKey.HeartbeatInterval
923 )
924 ) {
925 ChargingStationConfigurationUtils.addConfigurationKey(
926 this,
927 StandardParametersKey.HeartbeatInterval,
928 '0'
929 );
930 }
931 if (
932 !ChargingStationConfigurationUtils.getConfigurationKey(
933 this,
934 StandardParametersKey.HeartBeatInterval
935 )
936 ) {
937 ChargingStationConfigurationUtils.addConfigurationKey(
938 this,
939 StandardParametersKey.HeartBeatInterval,
940 '0',
941 { visible: false }
942 );
943 }
944 if (
945 this.getSupervisionUrlOcppConfiguration() &&
946 !ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
947 ) {
948 ChargingStationConfigurationUtils.addConfigurationKey(
949 this,
950 this.getSupervisionUrlOcppKey(),
951 this.configuredSupervisionUrl.href,
952 { reboot: true }
953 );
954 } else if (
955 !this.getSupervisionUrlOcppConfiguration() &&
956 ChargingStationConfigurationUtils.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
957 ) {
958 ChargingStationConfigurationUtils.deleteConfigurationKey(
959 this,
960 this.getSupervisionUrlOcppKey(),
961 { save: false }
962 );
963 }
964 if (
965 this.stationInfo.amperageLimitationOcppKey &&
966 !ChargingStationConfigurationUtils.getConfigurationKey(
967 this,
968 this.stationInfo.amperageLimitationOcppKey
969 )
970 ) {
971 ChargingStationConfigurationUtils.addConfigurationKey(
972 this,
973 this.stationInfo.amperageLimitationOcppKey,
974 (
975 this.stationInfo.maximumAmperage *
976 ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
977 ).toString()
978 );
979 }
980 if (
981 !ChargingStationConfigurationUtils.getConfigurationKey(
982 this,
983 StandardParametersKey.SupportedFeatureProfiles
984 )
985 ) {
986 ChargingStationConfigurationUtils.addConfigurationKey(
987 this,
988 StandardParametersKey.SupportedFeatureProfiles,
989 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
990 );
991 }
992 ChargingStationConfigurationUtils.addConfigurationKey(
993 this,
994 StandardParametersKey.NumberOfConnectors,
995 this.getNumberOfConnectors().toString(),
996 { readonly: true },
997 { overwrite: true }
998 );
999 if (
1000 !ChargingStationConfigurationUtils.getConfigurationKey(
1001 this,
1002 StandardParametersKey.MeterValuesSampledData
1003 )
1004 ) {
1005 ChargingStationConfigurationUtils.addConfigurationKey(
1006 this,
1007 StandardParametersKey.MeterValuesSampledData,
1008 MeterValueMeasurand.ENERGY_ACTIVE_IMPORT_REGISTER
1009 );
1010 }
1011 if (
1012 !ChargingStationConfigurationUtils.getConfigurationKey(
1013 this,
1014 StandardParametersKey.ConnectorPhaseRotation
1015 )
1016 ) {
1017 const connectorPhaseRotation = [];
1018 for (const connectorId of this.connectors.keys()) {
1019 // AC/DC
1020 if (connectorId === 0 && this.getNumberOfPhases() === 0) {
1021 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1022 } else if (connectorId > 0 && this.getNumberOfPhases() === 0) {
1023 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1024 // AC
1025 } else if (connectorId > 0 && this.getNumberOfPhases() === 1) {
1026 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.NotApplicable}`);
1027 } else if (connectorId > 0 && this.getNumberOfPhases() === 3) {
1028 connectorPhaseRotation.push(`${connectorId}.${ConnectorPhaseRotation.RST}`);
1029 }
1030 }
1031 ChargingStationConfigurationUtils.addConfigurationKey(
1032 this,
1033 StandardParametersKey.ConnectorPhaseRotation,
1034 connectorPhaseRotation.toString()
1035 );
1036 }
1037 if (
1038 !ChargingStationConfigurationUtils.getConfigurationKey(
1039 this,
1040 StandardParametersKey.AuthorizeRemoteTxRequests
1041 )
1042 ) {
1043 ChargingStationConfigurationUtils.addConfigurationKey(
1044 this,
1045 StandardParametersKey.AuthorizeRemoteTxRequests,
1046 'true'
1047 );
1048 }
1049 if (
1050 !ChargingStationConfigurationUtils.getConfigurationKey(
1051 this,
1052 StandardParametersKey.LocalAuthListEnabled
1053 ) &&
1054 ChargingStationConfigurationUtils.getConfigurationKey(
1055 this,
1056 StandardParametersKey.SupportedFeatureProfiles
1057 )?.value.includes(SupportedFeatureProfiles.LocalAuthListManagement)
1058 ) {
1059 ChargingStationConfigurationUtils.addConfigurationKey(
1060 this,
1061 StandardParametersKey.LocalAuthListEnabled,
1062 'false'
1063 );
1064 }
1065 if (
1066 !ChargingStationConfigurationUtils.getConfigurationKey(
1067 this,
1068 StandardParametersKey.ConnectionTimeOut
1069 )
1070 ) {
1071 ChargingStationConfigurationUtils.addConfigurationKey(
1072 this,
1073 StandardParametersKey.ConnectionTimeOut,
1074 Constants.DEFAULT_CONNECTION_TIMEOUT.toString()
1075 );
1076 }
1077 this.saveOcppConfiguration();
1078 }
1079
1080 private initializeConnectors(
1081 stationInfo: ChargingStationInfo,
1082 configuredMaxConnectors: number,
1083 templateMaxConnectors: number
1084 ): void {
1085 if (!stationInfo?.Connectors && this.connectors.size === 0) {
1086 const logMsg = `${this.logPrefix()} No already defined connectors and charging station information from template ${
1087 this.templateFile
1088 } with no connectors configuration defined`;
1089 logger.error(logMsg);
1090 throw new BaseError(logMsg);
1091 }
1092 if (!stationInfo?.Connectors[0]) {
1093 logger.warn(
1094 `${this.logPrefix()} Charging station information from template ${
1095 this.templateFile
1096 } with no connector Id 0 configuration`
1097 );
1098 }
1099 if (stationInfo?.Connectors) {
1100 const connectorsConfigHash = crypto
1101 .createHash(Constants.DEFAULT_HASH_ALGORITHM)
1102 .update(JSON.stringify(stationInfo?.Connectors) + configuredMaxConnectors.toString())
1103 .digest('hex');
1104 const connectorsConfigChanged =
1105 this.connectors?.size !== 0 && this.connectorsConfigurationHash !== connectorsConfigHash;
1106 if (this.connectors?.size === 0 || connectorsConfigChanged) {
1107 connectorsConfigChanged && this.connectors.clear();
1108 this.connectorsConfigurationHash = connectorsConfigHash;
1109 // Add connector Id 0
1110 let lastConnector = '0';
1111 for (lastConnector in stationInfo?.Connectors) {
1112 const lastConnectorId = Utils.convertToInt(lastConnector);
1113 if (
1114 lastConnectorId === 0 &&
1115 this.getUseConnectorId0(stationInfo) &&
1116 stationInfo?.Connectors[lastConnector]
1117 ) {
1118 this.connectors.set(
1119 lastConnectorId,
1120 Utils.cloneObject<ConnectorStatus>(stationInfo?.Connectors[lastConnector])
1121 );
1122 this.getConnectorStatus(lastConnectorId).availability = AvailabilityType.OPERATIVE;
1123 if (Utils.isUndefined(this.getConnectorStatus(lastConnectorId)?.chargingProfiles)) {
1124 this.getConnectorStatus(lastConnectorId).chargingProfiles = [];
1125 }
1126 }
1127 }
1128 // Generate all connectors
1129 if ((stationInfo?.Connectors[0] ? templateMaxConnectors - 1 : templateMaxConnectors) > 0) {
1130 for (let index = 1; index <= configuredMaxConnectors; index++) {
1131 const randConnectorId = stationInfo?.randomConnectors
1132 ? Utils.getRandomInteger(Utils.convertToInt(lastConnector), 1)
1133 : index;
1134 this.connectors.set(
1135 index,
1136 Utils.cloneObject<ConnectorStatus>(stationInfo?.Connectors[randConnectorId])
1137 );
1138 this.getConnectorStatus(index).availability = AvailabilityType.OPERATIVE;
1139 if (Utils.isUndefined(this.getConnectorStatus(index)?.chargingProfiles)) {
1140 this.getConnectorStatus(index).chargingProfiles = [];
1141 }
1142 }
1143 }
1144 }
1145 } else {
1146 logger.warn(
1147 `${this.logPrefix()} Charging station information from template ${
1148 this.templateFile
1149 } with no connectors configuration defined, using already defined connectors`
1150 );
1151 }
1152 // Initialize transaction attributes on connectors
1153 for (const connectorId of this.connectors.keys()) {
1154 if (connectorId > 0 && !this.getConnectorStatus(connectorId)?.transactionStarted) {
1155 this.initializeConnectorStatus(connectorId);
1156 }
1157 }
1158 }
1159
1160 private getConfigurationFromFile(): ChargingStationConfiguration | null {
1161 let configuration: ChargingStationConfiguration = null;
1162 if (this.configurationFile && fs.existsSync(this.configurationFile)) {
1163 try {
1164 const measureId = `${FileType.ChargingStationConfiguration} read`;
1165 const beginId = PerformanceStatistics.beginMeasure(measureId);
1166 configuration = JSON.parse(
1167 fs.readFileSync(this.configurationFile, 'utf8')
1168 ) as ChargingStationConfiguration;
1169 PerformanceStatistics.endMeasure(measureId, beginId);
1170 } catch (error) {
1171 FileUtils.handleFileException(
1172 this.logPrefix(),
1173 FileType.ChargingStationConfiguration,
1174 this.configurationFile,
1175 error as NodeJS.ErrnoException
1176 );
1177 }
1178 }
1179 return configuration;
1180 }
1181
1182 private saveConfiguration(section?: Section): void {
1183 if (this.configurationFile) {
1184 try {
1185 if (!fs.existsSync(path.dirname(this.configurationFile))) {
1186 fs.mkdirSync(path.dirname(this.configurationFile), { recursive: true });
1187 }
1188 const configurationData: ChargingStationConfiguration =
1189 this.getConfigurationFromFile() ?? {};
1190 switch (section) {
1191 case Section.ocppConfiguration:
1192 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1193 break;
1194 case Section.stationInfo:
1195 if (configurationData?.stationInfo?.infoHash === this.stationInfo?.infoHash) {
1196 logger.debug(
1197 `${this.logPrefix()} Not saving unchanged charging station information to configuration file ${
1198 this.configurationFile
1199 }`
1200 );
1201 return;
1202 }
1203 configurationData.stationInfo = this.stationInfo;
1204 break;
1205 default:
1206 configurationData.configurationKey = this.ocppConfiguration.configurationKey;
1207 if (configurationData?.stationInfo?.infoHash !== this.stationInfo?.infoHash) {
1208 configurationData.stationInfo = this.stationInfo;
1209 }
1210 break;
1211 }
1212 const measureId = `${FileType.ChargingStationConfiguration} write`;
1213 const beginId = PerformanceStatistics.beginMeasure(measureId);
1214 const fileDescriptor = fs.openSync(this.configurationFile, 'w');
1215 fs.writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
1216 fs.closeSync(fileDescriptor);
1217 PerformanceStatistics.endMeasure(measureId, beginId);
1218 } catch (error) {
1219 FileUtils.handleFileException(
1220 this.logPrefix(),
1221 FileType.ChargingStationConfiguration,
1222 this.configurationFile,
1223 error as NodeJS.ErrnoException
1224 );
1225 }
1226 } else {
1227 logger.error(
1228 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1229 );
1230 }
1231 }
1232
1233 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration | null {
1234 return this.getTemplateFromFile()?.Configuration ?? null;
1235 }
1236
1237 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration | null {
1238 let configuration: ChargingStationConfiguration = null;
1239 if (this.getOcppPersistentConfiguration()) {
1240 const configurationFromFile = this.getConfigurationFromFile();
1241 configuration = configurationFromFile?.configurationKey && configurationFromFile;
1242 }
1243 configuration && delete configuration.stationInfo;
1244 return configuration;
1245 }
1246
1247 private getOcppConfiguration(): ChargingStationOcppConfiguration | null {
1248 let ocppConfiguration: ChargingStationOcppConfiguration = this.getOcppConfigurationFromFile();
1249 if (!ocppConfiguration) {
1250 ocppConfiguration = this.getOcppConfigurationFromTemplate();
1251 }
1252 return ocppConfiguration;
1253 }
1254
1255 private async onOpen(): Promise<void> {
1256 if (this.isWebSocketConnectionOpened()) {
1257 logger.info(
1258 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1259 );
1260 if (!this.isRegistered()) {
1261 // Send BootNotification
1262 let registrationRetryCount = 0;
1263 do {
1264 this.bootNotificationResponse = await this.ocppRequestService.requestHandler<
1265 BootNotificationRequest,
1266 BootNotificationResponse
1267 >(
1268 this,
1269 RequestCommand.BOOT_NOTIFICATION,
1270 {
1271 chargePointModel: this.bootNotificationRequest.chargePointModel,
1272 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1273 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1274 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1275 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1276 iccid: this.bootNotificationRequest.iccid,
1277 imsi: this.bootNotificationRequest.imsi,
1278 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1279 meterType: this.bootNotificationRequest.meterType,
1280 },
1281 { skipBufferingOnError: true }
1282 );
1283 if (!this.isRegistered()) {
1284 this.getRegistrationMaxRetries() !== -1 && registrationRetryCount++;
1285 await Utils.sleep(
1286 this.bootNotificationResponse?.interval
1287 ? this.bootNotificationResponse.interval * 1000
1288 : Constants.OCPP_DEFAULT_BOOT_NOTIFICATION_INTERVAL
1289 );
1290 }
1291 } while (
1292 !this.isRegistered() &&
1293 (registrationRetryCount <= this.getRegistrationMaxRetries() ||
1294 this.getRegistrationMaxRetries() === -1)
1295 );
1296 }
1297 if (this.isRegistered()) {
1298 if (this.isInAcceptedState()) {
1299 await this.startMessageSequence();
1300 this.wsConnectionRestarted && this.flushMessageBuffer();
1301 }
1302 } else {
1303 logger.error(
1304 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1305 );
1306 }
1307 this.stopped && (this.stopped = false);
1308 this.autoReconnectRetryCount = 0;
1309 this.wsConnectionRestarted = false;
1310 } else {
1311 logger.warn(
1312 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1313 );
1314 }
1315 }
1316
1317 private async onClose(code: number, reason: string): Promise<void> {
1318 switch (code) {
1319 // Normal close
1320 case WebSocketCloseEventStatusCode.CLOSE_NORMAL:
1321 case WebSocketCloseEventStatusCode.CLOSE_NO_STATUS:
1322 logger.info(
1323 `${this.logPrefix()} WebSocket normally closed with status '${ChargingStationUtils.getWebSocketCloseEventStatusString(
1324 code
1325 )}' and reason '${reason}'`
1326 );
1327 this.autoReconnectRetryCount = 0;
1328 break;
1329 // Abnormal close
1330 default:
1331 logger.error(
1332 `${this.logPrefix()} WebSocket abnormally closed with status '${ChargingStationUtils.getWebSocketCloseEventStatusString(
1333 code
1334 )}' and reason '${reason}'`
1335 );
1336 await this.reconnect(code);
1337 break;
1338 }
1339 }
1340
1341 private async onMessage(data: Data): Promise<void> {
1342 let messageType: number;
1343 let messageId: string;
1344 let commandName: IncomingRequestCommand;
1345 let commandPayload: JsonType;
1346 let errorType: ErrorType;
1347 let errorMessage: string;
1348 let errorDetails: JsonType;
1349 let responseCallback: (payload: JsonType, requestPayload: JsonType) => void;
1350 let errorCallback: (error: OCPPError, requestStatistic?: boolean) => void;
1351 let requestCommandName: RequestCommand | IncomingRequestCommand;
1352 let requestPayload: JsonType;
1353 let cachedRequest: CachedRequest;
1354 let errMsg: string;
1355 try {
1356 const request = JSON.parse(data.toString()) as IncomingRequest | Response | ErrorResponse;
1357 if (Utils.isIterable(request)) {
1358 [messageType, messageId] = request;
1359 // Check the type of message
1360 switch (messageType) {
1361 // Incoming Message
1362 case MessageType.CALL_MESSAGE:
1363 [, , commandName, commandPayload] = request as IncomingRequest;
1364 if (this.getEnableStatistics()) {
1365 this.performanceStatistics.addRequestStatistic(commandName, messageType);
1366 }
1367 logger.debug(
1368 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1369 request
1370 )}`
1371 );
1372 // Process the message
1373 await this.ocppIncomingRequestService.incomingRequestHandler(
1374 this,
1375 messageId,
1376 commandName,
1377 commandPayload
1378 );
1379 break;
1380 // Outcome Message
1381 case MessageType.CALL_RESULT_MESSAGE:
1382 [, , commandPayload] = request as Response;
1383 if (!this.requests.has(messageId)) {
1384 // Error
1385 throw new OCPPError(
1386 ErrorType.INTERNAL_ERROR,
1387 `Response for unknown message id ${messageId}`,
1388 null,
1389 commandPayload
1390 );
1391 }
1392 // Respond
1393 cachedRequest = this.requests.get(messageId);
1394 if (Utils.isIterable(cachedRequest)) {
1395 [responseCallback, , requestCommandName, requestPayload] = cachedRequest;
1396 } else {
1397 throw new OCPPError(
1398 ErrorType.PROTOCOL_ERROR,
1399 `Cached request for message id ${messageId} response is not iterable`,
1400 null,
1401 cachedRequest as unknown as JsonType
1402 );
1403 }
1404 logger.debug(
1405 `${this.logPrefix()} << Command '${
1406 requestCommandName ?? ''
1407 }' received response payload: ${JSON.stringify(request)}`
1408 );
1409 responseCallback(commandPayload, requestPayload);
1410 break;
1411 // Error Message
1412 case MessageType.CALL_ERROR_MESSAGE:
1413 [, , errorType, errorMessage, errorDetails] = request as ErrorResponse;
1414 if (!this.requests.has(messageId)) {
1415 // Error
1416 throw new OCPPError(
1417 ErrorType.INTERNAL_ERROR,
1418 `Error response for unknown message id ${messageId}`,
1419 null,
1420 { errorType, errorMessage, errorDetails }
1421 );
1422 }
1423 cachedRequest = this.requests.get(messageId);
1424 if (Utils.isIterable(cachedRequest)) {
1425 [, errorCallback, requestCommandName] = cachedRequest;
1426 } else {
1427 throw new OCPPError(
1428 ErrorType.PROTOCOL_ERROR,
1429 `Cached request for message id ${messageId} error response is not iterable`,
1430 null,
1431 cachedRequest as unknown as JsonType
1432 );
1433 }
1434 logger.debug(
1435 `${this.logPrefix()} << Command '${
1436 requestCommandName ?? ''
1437 }' received error payload: ${JSON.stringify(request)}`
1438 );
1439 errorCallback(new OCPPError(errorType, errorMessage, requestCommandName, errorDetails));
1440 break;
1441 // Error
1442 default:
1443 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1444 errMsg = `${this.logPrefix()} Wrong message type ${messageType}`;
1445 logger.error(errMsg);
1446 throw new OCPPError(ErrorType.PROTOCOL_ERROR, errMsg);
1447 }
1448 } else {
1449 throw new OCPPError(ErrorType.PROTOCOL_ERROR, 'Incoming message is not iterable', null, {
1450 payload: request,
1451 });
1452 }
1453 } catch (error) {
1454 // Log
1455 logger.error(
1456 '%s Incoming OCPP message %j matching cached request %j processing error %j',
1457 this.logPrefix(),
1458 data.toString(),
1459 this.requests.get(messageId),
1460 error
1461 );
1462 // Send error
1463 messageType === MessageType.CALL_MESSAGE &&
1464 (await this.ocppRequestService.sendError(
1465 this,
1466 messageId,
1467 error as OCPPError,
1468 commandName ?? requestCommandName ?? null
1469 ));
1470 }
1471 }
1472
1473 private onPing(): void {
1474 logger.debug(this.logPrefix() + ' Received a WS ping (rfc6455) from the server');
1475 }
1476
1477 private onPong(): void {
1478 logger.debug(this.logPrefix() + ' Received a WS pong (rfc6455) from the server');
1479 }
1480
1481 private onError(error: WSError): void {
1482 logger.error(this.logPrefix() + ' WebSocket error: %j', error);
1483 }
1484
1485 private getUseConnectorId0(stationInfo?: ChargingStationInfo): boolean | undefined {
1486 const localStationInfo = stationInfo ?? this.stationInfo;
1487 return !Utils.isUndefined(localStationInfo.useConnectorId0)
1488 ? localStationInfo.useConnectorId0
1489 : true;
1490 }
1491
1492 private getNumberOfRunningTransactions(): number {
1493 let trxCount = 0;
1494 for (const connectorId of this.connectors.keys()) {
1495 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1496 trxCount++;
1497 }
1498 }
1499 return trxCount;
1500 }
1501
1502 // 0 for disabling
1503 private getConnectionTimeout(): number | undefined {
1504 if (
1505 ChargingStationConfigurationUtils.getConfigurationKey(
1506 this,
1507 StandardParametersKey.ConnectionTimeOut
1508 )
1509 ) {
1510 return (
1511 parseInt(
1512 ChargingStationConfigurationUtils.getConfigurationKey(
1513 this,
1514 StandardParametersKey.ConnectionTimeOut
1515 ).value
1516 ) ?? Constants.DEFAULT_CONNECTION_TIMEOUT
1517 );
1518 }
1519 return Constants.DEFAULT_CONNECTION_TIMEOUT;
1520 }
1521
1522 // -1 for unlimited, 0 for disabling
1523 private getAutoReconnectMaxRetries(): number | undefined {
1524 if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
1525 return this.stationInfo.autoReconnectMaxRetries;
1526 }
1527 if (!Utils.isUndefined(Configuration.getAutoReconnectMaxRetries())) {
1528 return Configuration.getAutoReconnectMaxRetries();
1529 }
1530 return -1;
1531 }
1532
1533 // 0 for disabling
1534 private getRegistrationMaxRetries(): number | undefined {
1535 if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
1536 return this.stationInfo.registrationMaxRetries;
1537 }
1538 return -1;
1539 }
1540
1541 private getPowerDivider(): number {
1542 let powerDivider = this.getNumberOfConnectors();
1543 if (this.stationInfo?.powerSharedByConnectors) {
1544 powerDivider = this.getNumberOfRunningTransactions();
1545 }
1546 return powerDivider;
1547 }
1548
1549 private getMaximumPower(stationInfo?: ChargingStationInfo): number {
1550 const localStationInfo = stationInfo ?? this.stationInfo;
1551 return (localStationInfo['maxPower'] as number) ?? localStationInfo.maximumPower;
1552 }
1553
1554 private getMaximumAmperage(stationInfo: ChargingStationInfo): number | undefined {
1555 const maximumPower = this.getMaximumPower(stationInfo);
1556 switch (this.getCurrentOutType(stationInfo)) {
1557 case CurrentType.AC:
1558 return ACElectricUtils.amperagePerPhaseFromPower(
1559 this.getNumberOfPhases(stationInfo),
1560 maximumPower / this.getNumberOfConnectors(),
1561 this.getVoltageOut(stationInfo)
1562 );
1563 case CurrentType.DC:
1564 return DCElectricUtils.amperage(maximumPower, this.getVoltageOut(stationInfo));
1565 }
1566 }
1567
1568 private getAmperageLimitation(): number | undefined {
1569 if (
1570 this.stationInfo.amperageLimitationOcppKey &&
1571 ChargingStationConfigurationUtils.getConfigurationKey(
1572 this,
1573 this.stationInfo.amperageLimitationOcppKey
1574 )
1575 ) {
1576 return (
1577 Utils.convertToInt(
1578 ChargingStationConfigurationUtils.getConfigurationKey(
1579 this,
1580 this.stationInfo.amperageLimitationOcppKey
1581 ).value
1582 ) / ChargingStationUtils.getAmperageLimitationUnitDivider(this.stationInfo)
1583 );
1584 }
1585 }
1586
1587 private async startMessageSequence(): Promise<void> {
1588 if (this.stationInfo.autoRegister) {
1589 await this.ocppRequestService.requestHandler<
1590 BootNotificationRequest,
1591 BootNotificationResponse
1592 >(
1593 this,
1594 RequestCommand.BOOT_NOTIFICATION,
1595 {
1596 chargePointModel: this.bootNotificationRequest.chargePointModel,
1597 chargePointVendor: this.bootNotificationRequest.chargePointVendor,
1598 chargeBoxSerialNumber: this.bootNotificationRequest.chargeBoxSerialNumber,
1599 firmwareVersion: this.bootNotificationRequest.firmwareVersion,
1600 chargePointSerialNumber: this.bootNotificationRequest.chargePointSerialNumber,
1601 iccid: this.bootNotificationRequest.iccid,
1602 imsi: this.bootNotificationRequest.imsi,
1603 meterSerialNumber: this.bootNotificationRequest.meterSerialNumber,
1604 meterType: this.bootNotificationRequest.meterType,
1605 },
1606 { skipBufferingOnError: true }
1607 );
1608 }
1609 // Start WebSocket ping
1610 this.startWebSocketPing();
1611 // Start heartbeat
1612 this.startHeartbeat();
1613 // Initialize connectors status
1614 for (const connectorId of this.connectors.keys()) {
1615 if (connectorId === 0) {
1616 continue;
1617 } else if (
1618 !this.stopped &&
1619 !this.getConnectorStatus(connectorId)?.status &&
1620 this.getConnectorStatus(connectorId)?.bootStatus
1621 ) {
1622 // Send status in template at startup
1623 await this.ocppRequestService.requestHandler<
1624 StatusNotificationRequest,
1625 StatusNotificationResponse
1626 >(this, RequestCommand.STATUS_NOTIFICATION, {
1627 connectorId,
1628 status: this.getConnectorStatus(connectorId).bootStatus,
1629 errorCode: ChargePointErrorCode.NO_ERROR,
1630 });
1631 this.getConnectorStatus(connectorId).status =
1632 this.getConnectorStatus(connectorId).bootStatus;
1633 } else if (
1634 this.stopped &&
1635 this.getConnectorStatus(connectorId)?.status &&
1636 this.getConnectorStatus(connectorId)?.bootStatus
1637 ) {
1638 // Send status in template after reset
1639 await this.ocppRequestService.requestHandler<
1640 StatusNotificationRequest,
1641 StatusNotificationResponse
1642 >(this, RequestCommand.STATUS_NOTIFICATION, {
1643 connectorId,
1644 status: this.getConnectorStatus(connectorId).bootStatus,
1645 errorCode: ChargePointErrorCode.NO_ERROR,
1646 });
1647 this.getConnectorStatus(connectorId).status =
1648 this.getConnectorStatus(connectorId).bootStatus;
1649 } else if (!this.stopped && this.getConnectorStatus(connectorId)?.status) {
1650 // Send previous status at template reload
1651 await this.ocppRequestService.requestHandler<
1652 StatusNotificationRequest,
1653 StatusNotificationResponse
1654 >(this, RequestCommand.STATUS_NOTIFICATION, {
1655 connectorId,
1656 status: this.getConnectorStatus(connectorId).status,
1657 errorCode: ChargePointErrorCode.NO_ERROR,
1658 });
1659 } else {
1660 // Send default status
1661 await this.ocppRequestService.requestHandler<
1662 StatusNotificationRequest,
1663 StatusNotificationResponse
1664 >(this, RequestCommand.STATUS_NOTIFICATION, {
1665 connectorId,
1666 status: ChargePointStatus.AVAILABLE,
1667 errorCode: ChargePointErrorCode.NO_ERROR,
1668 });
1669 this.getConnectorStatus(connectorId).status = ChargePointStatus.AVAILABLE;
1670 }
1671 }
1672 // Start the ATG
1673 this.startAutomaticTransactionGenerator();
1674 }
1675
1676 private startAutomaticTransactionGenerator() {
1677 if (this.getAutomaticTransactionGeneratorConfigurationFromTemplate()?.enable) {
1678 if (!this.automaticTransactionGenerator) {
1679 this.automaticTransactionGenerator = AutomaticTransactionGenerator.getInstance(
1680 this.getAutomaticTransactionGeneratorConfigurationFromTemplate(),
1681 this
1682 );
1683 }
1684 if (!this.automaticTransactionGenerator.started) {
1685 this.automaticTransactionGenerator.start();
1686 }
1687 }
1688 }
1689
1690 private stopAutomaticTransactionGenerator(): void {
1691 if (this.automaticTransactionGenerator?.started) {
1692 this.automaticTransactionGenerator.stop();
1693 this.automaticTransactionGenerator = null;
1694 }
1695 }
1696
1697 private async stopMessageSequence(
1698 reason: StopTransactionReason = StopTransactionReason.NONE
1699 ): Promise<void> {
1700 // Stop WebSocket ping
1701 this.stopWebSocketPing();
1702 // Stop heartbeat
1703 this.stopHeartbeat();
1704 // Stop ongoing transactions
1705 if (this.automaticTransactionGenerator?.configuration?.enable) {
1706 this.stopAutomaticTransactionGenerator();
1707 } else {
1708 for (const connectorId of this.connectors.keys()) {
1709 if (connectorId > 0 && this.getConnectorStatus(connectorId)?.transactionStarted) {
1710 const transactionId = this.getConnectorStatus(connectorId).transactionId;
1711 if (
1712 this.getBeginEndMeterValues() &&
1713 this.getOcppStrictCompliance() &&
1714 !this.getOutOfOrderEndMeterValues()
1715 ) {
1716 // FIXME: Implement OCPP version agnostic helpers
1717 const transactionEndMeterValue = OCPP16ServiceUtils.buildTransactionEndMeterValue(
1718 this,
1719 connectorId,
1720 this.getEnergyActiveImportRegisterByTransactionId(transactionId)
1721 );
1722 await this.ocppRequestService.requestHandler<MeterValuesRequest, MeterValuesResponse>(
1723 this,
1724 RequestCommand.METER_VALUES,
1725 {
1726 connectorId,
1727 transactionId,
1728 meterValue: transactionEndMeterValue,
1729 }
1730 );
1731 }
1732 await this.ocppRequestService.requestHandler<
1733 StopTransactionRequest,
1734 StopTransactionResponse
1735 >(this, RequestCommand.STOP_TRANSACTION, {
1736 transactionId,
1737 meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId),
1738 idTag: this.getTransactionIdTag(transactionId),
1739 reason,
1740 });
1741 }
1742 }
1743 }
1744 }
1745
1746 private startWebSocketPing(): void {
1747 const webSocketPingInterval: number = ChargingStationConfigurationUtils.getConfigurationKey(
1748 this,
1749 StandardParametersKey.WebSocketPingInterval
1750 )
1751 ? Utils.convertToInt(
1752 ChargingStationConfigurationUtils.getConfigurationKey(
1753 this,
1754 StandardParametersKey.WebSocketPingInterval
1755 ).value
1756 )
1757 : 0;
1758 if (webSocketPingInterval > 0 && !this.webSocketPingSetInterval) {
1759 this.webSocketPingSetInterval = setInterval(() => {
1760 if (this.isWebSocketConnectionOpened()) {
1761 this.wsConnection.ping((): void => {
1762 /* This is intentional */
1763 });
1764 }
1765 }, webSocketPingInterval * 1000);
1766 logger.info(
1767 this.logPrefix() +
1768 ' WebSocket ping started every ' +
1769 Utils.formatDurationSeconds(webSocketPingInterval)
1770 );
1771 } else if (this.webSocketPingSetInterval) {
1772 logger.info(
1773 this.logPrefix() +
1774 ' WebSocket ping every ' +
1775 Utils.formatDurationSeconds(webSocketPingInterval) +
1776 ' already started'
1777 );
1778 } else {
1779 logger.error(
1780 `${this.logPrefix()} WebSocket ping interval set to ${
1781 webSocketPingInterval
1782 ? Utils.formatDurationSeconds(webSocketPingInterval)
1783 : webSocketPingInterval
1784 }, not starting the WebSocket ping`
1785 );
1786 }
1787 }
1788
1789 private stopWebSocketPing(): void {
1790 if (this.webSocketPingSetInterval) {
1791 clearInterval(this.webSocketPingSetInterval);
1792 }
1793 }
1794
1795 private getConfiguredSupervisionUrl(): URL {
1796 const supervisionUrls = Utils.cloneObject<string | string[]>(
1797 this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
1798 );
1799 if (!Utils.isEmptyArray(supervisionUrls)) {
1800 let urlIndex = 0;
1801 switch (Configuration.getSupervisionUrlDistribution()) {
1802 case SupervisionUrlDistribution.ROUND_ROBIN:
1803 urlIndex = (this.index - 1) % supervisionUrls.length;
1804 break;
1805 case SupervisionUrlDistribution.RANDOM:
1806 // Get a random url
1807 urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
1808 break;
1809 case SupervisionUrlDistribution.SEQUENTIAL:
1810 if (this.index <= supervisionUrls.length) {
1811 urlIndex = this.index - 1;
1812 } else {
1813 logger.warn(
1814 `${this.logPrefix()} No more configured supervision urls available, using the first one`
1815 );
1816 }
1817 break;
1818 default:
1819 logger.error(
1820 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
1821 SupervisionUrlDistribution.ROUND_ROBIN
1822 }`
1823 );
1824 urlIndex = (this.index - 1) % supervisionUrls.length;
1825 break;
1826 }
1827 return new URL(supervisionUrls[urlIndex]);
1828 }
1829 return new URL(supervisionUrls as string);
1830 }
1831
1832 private getHeartbeatInterval(): number | undefined {
1833 const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1834 this,
1835 StandardParametersKey.HeartbeatInterval
1836 );
1837 if (HeartbeatInterval) {
1838 return Utils.convertToInt(HeartbeatInterval.value) * 1000;
1839 }
1840 const HeartBeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
1841 this,
1842 StandardParametersKey.HeartBeatInterval
1843 );
1844 if (HeartBeatInterval) {
1845 return Utils.convertToInt(HeartBeatInterval.value) * 1000;
1846 }
1847 !this.stationInfo.autoRegister &&
1848 logger.warn(
1849 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
1850 Constants.DEFAULT_HEARTBEAT_INTERVAL
1851 }`
1852 );
1853 return Constants.DEFAULT_HEARTBEAT_INTERVAL;
1854 }
1855
1856 private stopHeartbeat(): void {
1857 if (this.heartbeatSetInterval) {
1858 clearInterval(this.heartbeatSetInterval);
1859 }
1860 }
1861
1862 private openWSConnection(
1863 options: WsOptions = this.stationInfo?.wsOptions ?? {},
1864 forceCloseOpened = false
1865 ): void {
1866 options.handshakeTimeout = options?.handshakeTimeout ?? this.getConnectionTimeout() * 1000;
1867 if (
1868 !Utils.isNullOrUndefined(this.stationInfo.supervisionUser) &&
1869 !Utils.isNullOrUndefined(this.stationInfo.supervisionPassword)
1870 ) {
1871 options.auth = `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
1872 }
1873 if (this.isWebSocketConnectionOpened() && forceCloseOpened) {
1874 this.wsConnection.close();
1875 }
1876 let protocol: string;
1877 switch (this.getOcppVersion()) {
1878 case OCPPVersion.VERSION_16:
1879 protocol = 'ocpp' + OCPPVersion.VERSION_16;
1880 break;
1881 default:
1882 this.handleUnsupportedVersion(this.getOcppVersion());
1883 break;
1884 }
1885 this.wsConnection = new WebSocket(this.wsConnectionUrl, protocol, options);
1886 logger.info(
1887 this.logPrefix() + ' Open OCPP connection to URL ' + this.wsConnectionUrl.toString()
1888 );
1889 }
1890
1891 private stopMeterValues(connectorId: number) {
1892 if (this.getConnectorStatus(connectorId)?.transactionSetInterval) {
1893 clearInterval(this.getConnectorStatus(connectorId).transactionSetInterval);
1894 }
1895 }
1896
1897 private getReconnectExponentialDelay(): boolean | undefined {
1898 return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
1899 ? this.stationInfo.reconnectExponentialDelay
1900 : false;
1901 }
1902
1903 private async reconnect(code: number): Promise<void> {
1904 // Stop WebSocket ping
1905 this.stopWebSocketPing();
1906 // Stop heartbeat
1907 this.stopHeartbeat();
1908 // Stop the ATG if needed
1909 if (this.automaticTransactionGenerator?.configuration?.stopOnConnectionFailure) {
1910 this.stopAutomaticTransactionGenerator();
1911 }
1912 if (
1913 this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries() ||
1914 this.getAutoReconnectMaxRetries() === -1
1915 ) {
1916 this.autoReconnectRetryCount++;
1917 const reconnectDelay = this.getReconnectExponentialDelay()
1918 ? Utils.exponentialDelay(this.autoReconnectRetryCount)
1919 : this.getConnectionTimeout() * 1000;
1920 const reconnectTimeout = reconnectDelay - 100 > 0 && reconnectDelay;
1921 logger.error(
1922 `${this.logPrefix()} WebSocket: connection retry in ${Utils.roundTo(
1923 reconnectDelay,
1924 2
1925 )}ms, timeout ${reconnectTimeout}ms`
1926 );
1927 await Utils.sleep(reconnectDelay);
1928 logger.error(
1929 this.logPrefix() +
1930 ' WebSocket: reconnecting try #' +
1931 this.autoReconnectRetryCount.toString()
1932 );
1933 this.openWSConnection(
1934 { ...(this.stationInfo?.wsOptions ?? {}), handshakeTimeout: reconnectTimeout },
1935 true
1936 );
1937 this.wsConnectionRestarted = true;
1938 } else if (this.getAutoReconnectMaxRetries() !== -1) {
1939 logger.error(
1940 `${this.logPrefix()} WebSocket reconnect failure: maximum retries reached (${
1941 this.autoReconnectRetryCount
1942 }) or retry disabled (${this.getAutoReconnectMaxRetries()})`
1943 );
1944 }
1945 }
1946
1947 private getAutomaticTransactionGeneratorConfigurationFromTemplate(): AutomaticTransactionGeneratorConfiguration | null {
1948 return this.getTemplateFromFile()?.AutomaticTransactionGenerator ?? null;
1949 }
1950
1951 private initializeConnectorStatus(connectorId: number): void {
1952 this.getConnectorStatus(connectorId).idTagLocalAuthorized = false;
1953 this.getConnectorStatus(connectorId).idTagAuthorized = false;
1954 this.getConnectorStatus(connectorId).transactionRemoteStarted = false;
1955 this.getConnectorStatus(connectorId).transactionStarted = false;
1956 this.getConnectorStatus(connectorId).energyActiveImportRegisterValue = 0;
1957 this.getConnectorStatus(connectorId).transactionEnergyActiveImportRegisterValue = 0;
1958 }
1959 }