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