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