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