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