1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { createHash
} from
'node:crypto';
4 import { type FSWatcher
, existsSync
, mkdirSync
, readFileSync
, writeFileSync
} from
'node:fs';
5 import { dirname
, join
} from
'node:path';
6 import { URL
} from
'node:url';
7 import { parentPort
} from
'node:worker_threads';
9 import { millisecondsToSeconds
, secondsToMilliseconds
} from
'date-fns';
10 import merge from
'just-merge';
11 import { type RawData
, WebSocket
} from
'ws';
13 import { AutomaticTransactionGenerator
} from
'./AutomaticTransactionGenerator';
14 import { ChargingStationWorkerBroadcastChannel
} from
'./broadcast-channel/ChargingStationWorkerBroadcastChannel';
17 deleteConfigurationKey
,
19 setConfigurationKeyValue
,
20 } from
'./ConfigurationKeyUtils';
24 checkConnectorsConfiguration
,
25 checkStationInfoConnectorStatus
,
27 createBootNotificationRequest
,
29 getAmperageLimitationUnitDivider
,
30 getBootConnectorStatus
,
31 getChargingStationConnectorChargingProfilesPowerLimit
,
37 getNumberOfReservableConnectors
,
38 getPhaseRotationValue
,
40 hasReservationExpired
,
41 initializeConnectorsMapStatus
,
42 propagateSerialNumber
,
43 removeExpiredReservations
,
44 stationTemplateToStationInfo
,
45 warnTemplateKeysDeprecation
,
47 import { IdTagsCache
} from
'./IdTagsCache';
49 OCPP16IncomingRequestService
,
51 OCPP16ResponseService
,
53 OCPP20IncomingRequestService
,
55 OCPP20ResponseService
,
56 type OCPPIncomingRequestService
,
57 type OCPPRequestService
,
60 import { SharedLRUCache
} from
'./SharedLRUCache';
61 import { BaseError
, OCPPError
} from
'../exception';
62 import { PerformanceStatistics
} from
'../performance';
64 type AutomaticTransactionGeneratorConfiguration
,
66 type BootNotificationRequest
,
67 type BootNotificationResponse
,
69 type ChargingStationConfiguration
,
70 type ChargingStationInfo
,
71 type ChargingStationOcppConfiguration
,
72 type ChargingStationTemplate
,
80 type EvseStatusConfiguration
,
83 type FirmwareStatusNotificationRequest
,
84 type FirmwareStatusNotificationResponse
,
86 type HeartbeatRequest
,
87 type HeartbeatResponse
,
89 type IncomingRequestCommand
,
94 type MeterValuesRequest
,
95 type MeterValuesResponse
,
99 RegistrationStatusEnumType
,
103 ReservationTerminationReason
,
105 StandardParametersKey
,
107 type StatusNotificationRequest
,
108 type StatusNotificationResponse
,
109 StopTransactionReason
,
110 type StopTransactionRequest
,
111 type StopTransactionResponse
,
112 SupervisionUrlDistribution
,
113 SupportedFeatureProfiles
,
116 WebSocketCloseEventStatusCode
,
126 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
127 buildConnectorsStatus
,
136 formatDurationMilliSeconds
,
137 formatDurationSeconds
,
139 getWebSocketCloseEventStatusString
,
155 export class ChargingStation
{
156 public readonly index
: number;
157 public readonly templateFile
: string;
158 public stationInfo
!: ChargingStationInfo
;
159 public started
: boolean;
160 public starting
: boolean;
161 public idTagsCache
: IdTagsCache
;
162 public automaticTransactionGenerator
!: AutomaticTransactionGenerator
| undefined;
163 public ocppConfiguration
!: ChargingStationOcppConfiguration
| undefined;
164 public wsConnection
!: WebSocket
| null;
165 public readonly connectors
: Map
<number, ConnectorStatus
>;
166 public readonly evses
: Map
<number, EvseStatus
>;
167 public readonly requests
: Map
<string, CachedRequest
>;
168 public performanceStatistics
!: PerformanceStatistics
| undefined;
169 public heartbeatSetInterval
?: NodeJS
.Timeout
;
170 public ocppRequestService
!: OCPPRequestService
;
171 public bootNotificationRequest
!: BootNotificationRequest
;
172 public bootNotificationResponse
!: BootNotificationResponse
| undefined;
173 public powerDivider
!: number;
174 private stopping
: boolean;
175 private configurationFile
!: string;
176 private configurationFileHash
!: string;
177 private connectorsConfigurationHash
!: string;
178 private evsesConfigurationHash
!: string;
179 private automaticTransactionGeneratorConfiguration
?: AutomaticTransactionGeneratorConfiguration
;
180 private ocppIncomingRequestService
!: OCPPIncomingRequestService
;
181 private readonly messageBuffer
: Set
<string>;
182 private configuredSupervisionUrl
!: URL
;
183 private wsConnectionRestarted
: boolean;
184 private autoReconnectRetryCount
: number;
185 private templateFileWatcher
!: FSWatcher
| undefined;
186 private templateFileHash
!: string;
187 private readonly sharedLRUCache
: SharedLRUCache
;
188 private webSocketPingSetInterval
?: NodeJS
.Timeout
;
189 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
;
190 private reservationExpirationSetInterval
?: NodeJS
.Timeout
;
192 constructor(index
: number, templateFile
: string) {
193 this.started
= false;
194 this.starting
= false;
195 this.stopping
= false;
196 this.wsConnectionRestarted
= false;
197 this.autoReconnectRetryCount
= 0;
199 this.templateFile
= templateFile
;
200 this.connectors
= new Map
<number, ConnectorStatus
>();
201 this.evses
= new Map
<number, EvseStatus
>();
202 this.requests
= new Map
<string, CachedRequest
>();
203 this.messageBuffer
= new Set
<string>();
204 this.sharedLRUCache
= SharedLRUCache
.getInstance();
205 this.idTagsCache
= IdTagsCache
.getInstance();
206 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this);
211 public get
hasEvses(): boolean {
212 return this.connectors
.size
=== 0 && this.evses
.size
> 0;
215 private get
wsConnectionUrl(): URL
{
218 this.getSupervisionUrlOcppConfiguration() &&
219 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
220 isNotEmptyString(getConfigurationKey(this, this.getSupervisionUrlOcppKey())?.value)
221 ? getConfigurationKey(this, this.getSupervisionUrlOcppKey())!.value
222 : this.configuredSupervisionUrl.href
223 }/${this.stationInfo.chargingStationId}`,
227 public logPrefix
= (): string => {
230 (isNotEmptyString(this?.stationInfo?.chargingStationId)
231 ? this?.stationInfo?.chargingStationId
232 : getChargingStationId(this.index, this.getTemplateFromFile()!)) ??
233 'Error at building log prefix'
238 public hasIdTags(): boolean {
239 return isNotEmptyArray(this.idTagsCache
.getIdTags(getIdTagsFile(this.stationInfo
)!));
242 public getEnableStatistics(): boolean {
243 return this.stationInfo
.enableStatistics
?? false;
246 public getRemoteAuthorization(): boolean {
247 return this.stationInfo
.remoteAuthorization
?? true;
250 public getNumberOfPhases(stationInfo
?: ChargingStationInfo
): number {
251 const localStationInfo
: ChargingStationInfo
= stationInfo
?? this.stationInfo
;
252 switch (this.getCurrentOutType(stationInfo
)) {
254 return !isUndefined(localStationInfo
.numberOfPhases
) ? localStationInfo
.numberOfPhases
! : 3;
260 public isWebSocketConnectionOpened(): boolean {
261 return this?.wsConnection
?.readyState
=== WebSocket
.OPEN
;
264 public getRegistrationStatus(): RegistrationStatusEnumType
| undefined {
265 return this?.bootNotificationResponse
?.status;
268 public inUnknownState(): boolean {
269 return isNullOrUndefined(this?.bootNotificationResponse
?.status);
272 public inPendingState(): boolean {
273 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
;
276 public inAcceptedState(): boolean {
277 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
;
280 public inRejectedState(): boolean {
281 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
;
284 public isRegistered(): boolean {
286 this.inUnknownState() === false &&
287 (this.inAcceptedState() === true || this.inPendingState() === true)
291 public isChargingStationAvailable(): boolean {
292 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
;
295 public hasConnector(connectorId
: number): boolean {
297 for (const evseStatus
of this.evses
.values()) {
298 if (evseStatus
.connectors
.has(connectorId
)) {
304 return this.connectors
.has(connectorId
);
307 public isConnectorAvailable(connectorId
: number): boolean {
310 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
314 public getNumberOfConnectors(): number {
316 let numberOfConnectors
= 0;
317 for (const [evseId
, evseStatus
] of this.evses
) {
319 numberOfConnectors
+= evseStatus
.connectors
.size
;
322 return numberOfConnectors
;
324 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
;
327 public getNumberOfEvses(): number {
328 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
;
331 public getConnectorStatus(connectorId
: number): ConnectorStatus
| undefined {
333 for (const evseStatus
of this.evses
.values()) {
334 if (evseStatus
.connectors
.has(connectorId
)) {
335 return evseStatus
.connectors
.get(connectorId
);
340 return this.connectors
.get(connectorId
);
343 public getCurrentOutType(stationInfo
?: ChargingStationInfo
): CurrentType
{
344 return (stationInfo
?? this.stationInfo
)?.currentOutType
?? CurrentType
.AC
;
347 public getOcppStrictCompliance(): boolean {
348 return this.stationInfo
?.ocppStrictCompliance
?? true;
351 public getVoltageOut(stationInfo
?: ChargingStationInfo
): number {
352 const defaultVoltageOut
= getDefaultVoltageOut(
353 this.getCurrentOutType(stationInfo
),
357 return (stationInfo
?? this.stationInfo
).voltageOut
?? defaultVoltageOut
;
360 public getMaximumPower(stationInfo
?: ChargingStationInfo
): number {
361 return (stationInfo
?? this.stationInfo
).maximumPower
!;
364 public getConnectorMaximumAvailablePower(connectorId
: number): number {
365 let connectorAmperageLimitationPowerLimit
: number | undefined;
367 !isNullOrUndefined(this.getAmperageLimitation()) &&
368 this.getAmperageLimitation()! < this.stationInfo
.maximumAmperage
!
370 connectorAmperageLimitationPowerLimit
=
371 (this.getCurrentOutType() === CurrentType
.AC
372 ? ACElectricUtils
.powerTotal(
373 this.getNumberOfPhases(),
374 this.getVoltageOut(),
375 this.getAmperageLimitation()! *
376 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
378 : DCElectricUtils
.power(this.getVoltageOut(), this.getAmperageLimitation()!)) /
381 const connectorMaximumPower
= this.getMaximumPower() / this.powerDivider
;
382 const connectorChargingProfilesPowerLimit
=
383 getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
);
385 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
386 isNaN(connectorAmperageLimitationPowerLimit
!)
388 : connectorAmperageLimitationPowerLimit
!,
389 isNaN(connectorChargingProfilesPowerLimit
!) ? Infinity : connectorChargingProfilesPowerLimit
!,
393 public getTransactionIdTag(transactionId
: number): string | undefined {
395 for (const evseStatus
of this.evses
.values()) {
396 for (const connectorStatus
of evseStatus
.connectors
.values()) {
397 if (connectorStatus
.transactionId
=== transactionId
) {
398 return connectorStatus
.transactionIdTag
;
403 for (const connectorId
of this.connectors
.keys()) {
404 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
405 return this.getConnectorStatus(connectorId
)?.transactionIdTag
;
411 public getNumberOfRunningTransactions(): number {
412 let numberOfRunningTransactions
= 0;
414 for (const [evseId
, evseStatus
] of this.evses
) {
418 for (const connectorStatus
of evseStatus
.connectors
.values()) {
419 if (connectorStatus
.transactionStarted
=== true) {
420 ++numberOfRunningTransactions
;
425 for (const connectorId
of this.connectors
.keys()) {
426 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
427 ++numberOfRunningTransactions
;
431 return numberOfRunningTransactions
;
434 public getOutOfOrderEndMeterValues(): boolean {
435 return this.stationInfo
?.outOfOrderEndMeterValues
?? false;
438 public getBeginEndMeterValues(): boolean {
439 return this.stationInfo
?.beginEndMeterValues
?? false;
442 public getMeteringPerTransaction(): boolean {
443 return this.stationInfo
?.meteringPerTransaction
?? true;
446 public getTransactionDataMeterValues(): boolean {
447 return this.stationInfo
?.transactionDataMeterValues
?? false;
450 public getMainVoltageMeterValues(): boolean {
451 return this.stationInfo
?.mainVoltageMeterValues
?? true;
454 public getPhaseLineToLineVoltageMeterValues(): boolean {
455 return this.stationInfo
?.phaseLineToLineVoltageMeterValues
?? false;
458 public getCustomValueLimitationMeterValues(): boolean {
459 return this.stationInfo
?.customValueLimitationMeterValues
?? true;
462 public getConnectorIdByTransactionId(transactionId
: number): number | undefined {
464 for (const evseStatus
of this.evses
.values()) {
465 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
466 if (connectorStatus
.transactionId
=== transactionId
) {
472 for (const connectorId
of this.connectors
.keys()) {
473 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
480 public getEnergyActiveImportRegisterByTransactionId(
481 transactionId
: number,
484 return this.getEnergyActiveImportRegister(
485 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)!)!,
490 public getEnergyActiveImportRegisterByConnectorId(connectorId
: number, rounded
= false): number {
491 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
)!, rounded
);
494 public getAuthorizeRemoteTxRequests(): boolean {
495 const authorizeRemoteTxRequests
= getConfigurationKey(
497 StandardParametersKey
.AuthorizeRemoteTxRequests
,
499 return authorizeRemoteTxRequests
? convertToBoolean(authorizeRemoteTxRequests
.value
) : false;
502 public getLocalAuthListEnabled(): boolean {
503 const localAuthListEnabled
= getConfigurationKey(
505 StandardParametersKey
.LocalAuthListEnabled
,
507 return localAuthListEnabled
? convertToBoolean(localAuthListEnabled
.value
) : false;
510 public getHeartbeatInterval(): number {
511 const HeartbeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
);
512 if (HeartbeatInterval
) {
513 return secondsToMilliseconds(convertToInt(HeartbeatInterval
.value
));
515 const HeartBeatInterval
= getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
);
516 if (HeartBeatInterval
) {
517 return secondsToMilliseconds(convertToInt(HeartBeatInterval
.value
));
519 this.stationInfo
?.autoRegister
=== false &&
521 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
522 Constants.DEFAULT_HEARTBEAT_INTERVAL
525 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
;
528 public setSupervisionUrl(url
: string): void {
530 this.getSupervisionUrlOcppConfiguration() &&
531 isNotEmptyString(this.getSupervisionUrlOcppKey())
533 setConfigurationKeyValue(this, this.getSupervisionUrlOcppKey(), url
);
535 this.stationInfo
.supervisionUrls
= url
;
536 this.saveStationInfo();
537 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
541 public startHeartbeat(): void {
542 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval
) {
543 this.heartbeatSetInterval
= setInterval(() => {
544 this.ocppRequestService
545 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
548 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
552 }, this.getHeartbeatInterval());
554 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
555 this.getHeartbeatInterval(),
558 } else if (this.heartbeatSetInterval
) {
560 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
561 this.getHeartbeatInterval(),
566 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`,
571 public restartHeartbeat(): void {
573 this.stopHeartbeat();
575 this.startHeartbeat();
578 public restartWebSocketPing(): void {
579 // Stop WebSocket ping
580 this.stopWebSocketPing();
581 // Start WebSocket ping
582 this.startWebSocketPing();
585 public startMeterValues(connectorId
: number, interval
: number): void {
586 if (connectorId
=== 0) {
588 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}`,
592 if (!this.getConnectorStatus(connectorId
)) {
594 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
599 if (this.getConnectorStatus(connectorId
)?.transactionStarted
=== false) {
601 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`,
605 this.getConnectorStatus(connectorId
)?.transactionStarted
=== true &&
606 isNullOrUndefined(this.getConnectorStatus(connectorId
)?.transactionId
)
609 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`,
614 this.getConnectorStatus(connectorId
)!.transactionSetInterval
= setInterval(() => {
615 // FIXME: Implement OCPP version agnostic helpers
616 const meterValue
: MeterValue
= OCPP16ServiceUtils
.buildMeterValue(
619 this.getConnectorStatus(connectorId
)!.transactionId
!,
622 this.ocppRequestService
623 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
625 RequestCommand
.METER_VALUES
,
628 transactionId
: this.getConnectorStatus(connectorId
)?.transactionId
,
629 meterValue
: [meterValue
],
634 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
641 `${this.logPrefix()} Charging station ${
642 StandardParametersKey.MeterValueSampleInterval
643 } configuration set to ${interval}, not sending MeterValues`,
648 public stopMeterValues(connectorId
: number) {
649 if (this.getConnectorStatus(connectorId
)?.transactionSetInterval
) {
650 clearInterval(this.getConnectorStatus(connectorId
)?.transactionSetInterval
);
654 public start(): void {
655 if (this.started
=== false) {
656 if (this.starting
=== false) {
657 this.starting
= true;
658 if (this.getEnableStatistics() === true) {
659 this.performanceStatistics
?.start();
661 if (hasFeatureProfile(this, SupportedFeatureProfiles
.Reservation
)) {
662 this.startReservationExpirationSetInterval();
664 this.openWSConnection();
665 // Monitor charging station template file
666 this.templateFileWatcher
= watchJsonFile(
668 FileType
.ChargingStationTemplate
,
671 (event
, filename
): void => {
672 if (isNotEmptyString(filename
) && event
=== 'change') {
675 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
677 } file have changed, reload`,
679 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
682 this.idTagsCache
.deleteIdTags(getIdTagsFile(this.stationInfo
)!);
684 this.stopAutomaticTransactionGenerator();
685 delete this.automaticTransactionGeneratorConfiguration
;
686 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
687 this.startAutomaticTransactionGenerator();
689 if (this.getEnableStatistics() === true) {
690 this.performanceStatistics
?.restart();
692 this.performanceStatistics
?.stop();
694 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
697 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
705 parentPort
?.postMessage(buildStartedMessage(this));
706 this.starting
= false;
708 logger
.warn(`${this.logPrefix()} Charging station is already starting...`);
711 logger
.warn(`${this.logPrefix()} Charging station is already started...`);
715 public async stop(reason
?: StopTransactionReason
): Promise
<void> {
716 if (this.started
=== true) {
717 if (this.stopping
=== false) {
718 this.stopping
= true;
719 await this.stopMessageSequence(reason
);
720 this.closeWSConnection();
721 if (this.getEnableStatistics() === true) {
722 this.performanceStatistics
?.stop();
724 if (hasFeatureProfile(this, SupportedFeatureProfiles
.Reservation
)) {
725 this.stopReservationExpirationSetInterval();
727 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
728 this.templateFileWatcher
?.close();
729 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
730 delete this.bootNotificationResponse
;
731 this.started
= false;
732 this.saveConfiguration();
733 parentPort
?.postMessage(buildStoppedMessage(this));
734 this.stopping
= false;
736 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`);
739 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`);
743 public async reset(reason
?: StopTransactionReason
): Promise
<void> {
744 await this.stop(reason
);
745 await sleep(this.stationInfo
.resetTime
!);
750 public saveOcppConfiguration(): void {
751 if (this.getOcppPersistentConfiguration()) {
752 this.saveConfiguration();
756 public bufferMessage(message
: string): void {
757 this.messageBuffer
.add(message
);
760 public openWSConnection(
762 params
?: { closeOpened
?: boolean; terminateOpened
?: boolean },
765 handshakeTimeout
: secondsToMilliseconds(this.getConnectionTimeout()),
766 ...this.stationInfo
?.wsOptions
,
769 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
};
770 if (!checkChargingStation(this, this.logPrefix())) {
774 !isNullOrUndefined(this.stationInfo
.supervisionUser
) &&
775 !isNullOrUndefined(this.stationInfo
.supervisionPassword
)
777 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
779 if (params
?.closeOpened
) {
780 this.closeWSConnection();
782 if (params
?.terminateOpened
) {
783 this.terminateWSConnection();
786 if (this.isWebSocketConnectionOpened() === true) {
788 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`,
794 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`,
797 this.wsConnection
= new WebSocket(
798 this.wsConnectionUrl
,
799 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
803 // Handle WebSocket message
804 this.wsConnection
.on(
806 this.onMessage
.bind(this) as (this: WebSocket
, data
: RawData
, isBinary
: boolean) => void,
808 // Handle WebSocket error
809 this.wsConnection
.on(
811 this.onError
.bind(this) as (this: WebSocket
, error
: Error) => void,
813 // Handle WebSocket close
814 this.wsConnection
.on(
816 this.onClose
.bind(this) as (this: WebSocket
, code
: number, reason
: Buffer
) => void,
818 // Handle WebSocket open
819 this.wsConnection
.on('open', this.onOpen
.bind(this) as (this: WebSocket
) => void);
820 // Handle WebSocket ping
821 this.wsConnection
.on('ping', this.onPing
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
822 // Handle WebSocket pong
823 this.wsConnection
.on('pong', this.onPong
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
826 public closeWSConnection(): void {
827 if (this.isWebSocketConnectionOpened() === true) {
828 this.wsConnection
?.close();
829 this.wsConnection
= null;
833 public getAutomaticTransactionGeneratorConfiguration(): AutomaticTransactionGeneratorConfiguration
{
834 if (isNullOrUndefined(this.automaticTransactionGeneratorConfiguration
)) {
835 let automaticTransactionGeneratorConfiguration
:
836 | AutomaticTransactionGeneratorConfiguration
838 const automaticTransactionGeneratorConfigurationFromFile
=
839 this.getConfigurationFromFile()?.automaticTransactionGenerator
;
841 this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
842 automaticTransactionGeneratorConfigurationFromFile
844 automaticTransactionGeneratorConfiguration
=
845 automaticTransactionGeneratorConfigurationFromFile
;
847 automaticTransactionGeneratorConfiguration
=
848 this.getTemplateFromFile()?.AutomaticTransactionGenerator
;
850 this.automaticTransactionGeneratorConfiguration
= {
851 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
852 ...automaticTransactionGeneratorConfiguration
,
855 return this.automaticTransactionGeneratorConfiguration
!;
858 public getAutomaticTransactionGeneratorStatuses(): Status
[] | undefined {
859 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
;
862 public startAutomaticTransactionGenerator(connectorIds
?: number[]): void {
863 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this);
864 if (isNotEmptyArray(connectorIds
)) {
865 for (const connectorId
of connectorIds
!) {
866 this.automaticTransactionGenerator
?.startConnector(connectorId
);
869 this.automaticTransactionGenerator
?.start();
871 this.saveAutomaticTransactionGeneratorConfiguration();
872 parentPort
?.postMessage(buildUpdatedMessage(this));
875 public stopAutomaticTransactionGenerator(connectorIds
?: number[]): void {
876 if (isNotEmptyArray(connectorIds
)) {
877 for (const connectorId
of connectorIds
!) {
878 this.automaticTransactionGenerator
?.stopConnector(connectorId
);
881 this.automaticTransactionGenerator
?.stop();
883 this.saveAutomaticTransactionGeneratorConfiguration();
884 parentPort
?.postMessage(buildUpdatedMessage(this));
887 public async stopTransactionOnConnector(
889 reason
= StopTransactionReason
.NONE
,
890 ): Promise
<StopTransactionResponse
> {
891 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
;
893 this.getBeginEndMeterValues() === true &&
894 this.getOcppStrictCompliance() === true &&
895 this.getOutOfOrderEndMeterValues() === false
897 // FIXME: Implement OCPP version agnostic helpers
898 const transactionEndMeterValue
= OCPP16ServiceUtils
.buildTransactionEndMeterValue(
901 this.getEnergyActiveImportRegisterByTransactionId(transactionId
!),
903 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
905 RequestCommand
.METER_VALUES
,
909 meterValue
: [transactionEndMeterValue
],
913 return this.ocppRequestService
.requestHandler
<StopTransactionRequest
, StopTransactionResponse
>(
915 RequestCommand
.STOP_TRANSACTION
,
918 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
!, true),
924 public getReserveConnectorZeroSupported(): boolean {
925 return convertToBoolean(
926 getConfigurationKey(this, StandardParametersKey
.ReserveConnectorZeroSupported
)!.value
,
930 public async addReservation(reservation
: Reservation
): Promise
<void> {
931 const reservationFound
= this.getReservationBy('reservationId', reservation
.reservationId
);
932 if (!isUndefined(reservationFound
)) {
933 await this.removeReservation(
935 ReservationTerminationReason
.REPLACE_EXISTING
,
938 this.getConnectorStatus(reservation
.connectorId
)!.reservation
= reservation
;
939 await OCPPServiceUtils
.sendAndSetConnectorStatus(
941 reservation
.connectorId
,
942 ConnectorStatusEnum
.Reserved
,
944 { send
: reservation
.connectorId
!== 0 },
948 public async removeReservation(
949 reservation
: Reservation
,
950 reason
: ReservationTerminationReason
,
952 const connector
= this.getConnectorStatus(reservation
.connectorId
)!;
954 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
955 case ReservationTerminationReason
.TRANSACTION_STARTED
:
956 delete connector
.reservation
;
958 case ReservationTerminationReason
.RESERVATION_CANCELED
:
959 case ReservationTerminationReason
.REPLACE_EXISTING
:
960 case ReservationTerminationReason
.EXPIRED
:
961 await OCPPServiceUtils
.sendAndSetConnectorStatus(
963 reservation
.connectorId
,
964 ConnectorStatusEnum
.Available
,
966 { send
: reservation
.connectorId
!== 0 },
968 delete connector
.reservation
;
971 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
972 throw new BaseError(`Unknown reservation termination reason '${reason}'`);
976 public getReservationBy(
977 filterKey
: ReservationKey
,
978 value
: number | string,
979 ): Reservation
| undefined {
981 for (const evseStatus
of this.evses
.values()) {
982 for (const connectorStatus
of evseStatus
.connectors
.values()) {
983 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
984 return connectorStatus
.reservation
;
989 for (const connectorStatus
of this.connectors
.values()) {
990 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
991 return connectorStatus
.reservation
;
997 public isConnectorReservable(
998 reservationId
: number,
1000 connectorId
?: number,
1002 const reservation
= this.getReservationBy('reservationId', reservationId
);
1003 const reservationExists
= !isUndefined(reservation
) && !hasReservationExpired(reservation
!);
1004 if (arguments.length
=== 1) {
1005 return !reservationExists
;
1006 } else if (arguments.length
> 1) {
1007 const userReservation
= !isUndefined(idTag
)
1008 ? this.getReservationBy('idTag', idTag
!)
1010 const userReservationExists
=
1011 !isUndefined(userReservation
) && !hasReservationExpired(userReservation
!);
1012 const notConnectorZero
= isUndefined(connectorId
) ? true : connectorId
! > 0;
1013 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0;
1015 !reservationExists
&& !userReservationExists
&& notConnectorZero
&& freeConnectorsAvailable
1021 private startReservationExpirationSetInterval(customInterval
?: number): void {
1022 const interval
= customInterval
?? Constants
.DEFAULT_RESERVATION_EXPIRATION_INTERVAL
;
1025 `${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
1029 this.reservationExpirationSetInterval
= setInterval((): void => {
1030 removeExpiredReservations(this).catch(Constants
.EMPTY_FUNCTION
);
1035 private stopReservationExpirationSetInterval(): void {
1036 if (!isNullOrUndefined(this.reservationExpirationSetInterval
)) {
1037 clearInterval(this.reservationExpirationSetInterval
);
1041 // private restartReservationExpiryDateSetInterval(): void {
1042 // this.stopReservationExpirationSetInterval();
1043 // this.startReservationExpirationSetInterval();
1046 private getNumberOfReservableConnectors(): number {
1047 let numberOfReservableConnectors
= 0;
1048 if (this.hasEvses
) {
1049 for (const evseStatus
of this.evses
.values()) {
1050 numberOfReservableConnectors
+= getNumberOfReservableConnectors(evseStatus
.connectors
);
1053 numberOfReservableConnectors
= getNumberOfReservableConnectors(this.connectors
);
1055 return numberOfReservableConnectors
- this.getNumberOfReservationsOnConnectorZero();
1058 private getNumberOfReservationsOnConnectorZero(): number {
1060 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1061 (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
) ||
1062 (!this.hasEvses
&& this.connectors
.get(0)?.reservation
)
1069 private flushMessageBuffer(): void {
1070 if (this.messageBuffer
.size
> 0) {
1071 for (const message
of this.messageBuffer
.values()) {
1072 let beginId
: string | undefined;
1073 let commandName
: RequestCommand
| undefined;
1074 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
;
1075 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
;
1077 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
;
1078 beginId
= PerformanceStatistics
.beginMeasure(commandName
);
1080 this.wsConnection
?.send(message
);
1081 isRequest
&& PerformanceStatistics
.endMeasure(commandName
!, beginId
!);
1083 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1085 )} payload sent: ${message}`,
1087 this.messageBuffer
.delete(message
);
1092 private getSupervisionUrlOcppConfiguration(): boolean {
1093 return this.stationInfo
.supervisionUrlOcppConfiguration
?? false;
1096 private getSupervisionUrlOcppKey(): string {
1097 return this.stationInfo
.supervisionUrlOcppKey
?? VendorParametersKey
.ConnectionUrl
;
1100 private getTemplateFromFile(): ChargingStationTemplate
| undefined {
1101 let template
: ChargingStationTemplate
| undefined;
1103 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1104 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
);
1106 const measureId
= `${FileType.ChargingStationTemplate} read`;
1107 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1108 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
;
1109 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1110 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1111 .update(JSON
.stringify(template
))
1113 this.sharedLRUCache
.setChargingStationTemplate(template
);
1114 this.templateFileHash
= template
.templateHash
;
1117 handleFileException(
1119 FileType
.ChargingStationTemplate
,
1120 error
as NodeJS
.ErrnoException
,
1127 private getStationInfoFromTemplate(): ChargingStationInfo
{
1128 const stationTemplate
: ChargingStationTemplate
= this.getTemplateFromFile()!;
1129 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1130 const warnTemplateKeysDeprecationOnce
= once(warnTemplateKeysDeprecation
, this);
1131 warnTemplateKeysDeprecationOnce(stationTemplate
, this.logPrefix(), this.templateFile
);
1132 if (stationTemplate
?.Connectors
) {
1133 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
);
1135 const stationInfo
: ChargingStationInfo
= stationTemplateToStationInfo(stationTemplate
);
1136 stationInfo
.hashId
= getHashId(this.index
, stationTemplate
);
1137 stationInfo
.chargingStationId
= getChargingStationId(this.index
, stationTemplate
);
1138 stationInfo
.ocppVersion
= stationTemplate
?.ocppVersion
?? OCPPVersion
.VERSION_16
;
1139 createSerialNumber(stationTemplate
, stationInfo
);
1140 if (isNotEmptyArray(stationTemplate
?.power
)) {
1141 stationTemplate
.power
= stationTemplate
.power
as number[];
1142 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
);
1143 stationInfo
.maximumPower
=
1144 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1145 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1146 : stationTemplate
.power
[powerArrayRandomIndex
];
1148 stationTemplate
.power
= stationTemplate
?.power
as number;
1149 stationInfo
.maximumPower
=
1150 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1151 ? stationTemplate
.power
* 1000
1152 : stationTemplate
.power
;
1154 stationInfo
.firmwareVersionPattern
=
1155 stationTemplate
?.firmwareVersionPattern
?? Constants
.SEMVER_PATTERN
;
1157 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1158 new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
!) === false
1161 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1163 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`,
1166 stationInfo
.firmwareUpgrade
= merge
<FirmwareUpgrade
>(
1173 stationTemplate
?.firmwareUpgrade
?? {},
1175 stationInfo
.resetTime
= !isNullOrUndefined(stationTemplate
?.resetTime
)
1176 ? secondsToMilliseconds(stationTemplate
.resetTime
!)
1177 : Constants
.CHARGING_STATION_DEFAULT_RESET_TIME
;
1178 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
);
1182 private getStationInfoFromFile(): ChargingStationInfo
| undefined {
1183 let stationInfo
: ChargingStationInfo
| undefined;
1184 if (this.getStationInfoPersistentConfiguration()) {
1185 stationInfo
= this.getConfigurationFromFile()?.stationInfo
;
1187 delete stationInfo
?.infoHash
;
1193 private getStationInfo(): ChargingStationInfo
{
1194 const stationInfoFromTemplate
: ChargingStationInfo
= this.getStationInfoFromTemplate();
1195 const stationInfoFromFile
: ChargingStationInfo
| undefined = this.getStationInfoFromFile();
1197 // 1. charging station info from template
1198 // 2. charging station info from configuration file
1199 if (stationInfoFromFile
?.templateHash
=== stationInfoFromTemplate
.templateHash
) {
1200 return stationInfoFromFile
!;
1202 stationInfoFromFile
&&
1203 propagateSerialNumber(
1204 this.getTemplateFromFile()!,
1205 stationInfoFromFile
,
1206 stationInfoFromTemplate
,
1208 return stationInfoFromTemplate
;
1211 private saveStationInfo(): void {
1212 if (this.getStationInfoPersistentConfiguration()) {
1213 this.saveConfiguration();
1217 private getOcppPersistentConfiguration(): boolean {
1218 return this.stationInfo
?.ocppPersistentConfiguration
?? true;
1221 private getStationInfoPersistentConfiguration(): boolean {
1222 return this.stationInfo
?.stationInfoPersistentConfiguration
?? true;
1225 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1226 return this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
?? true;
1229 private handleUnsupportedVersion(version
: OCPPVersion
) {
1230 const errorMsg
= `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
1231 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1232 throw new BaseError(errorMsg
);
1235 private initialize(): void {
1236 const stationTemplate
= this.getTemplateFromFile()!;
1237 checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1238 this.configurationFile
= join(
1239 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1240 `${getHashId(this.index, stationTemplate)}.json`,
1242 const chargingStationConfiguration
= this.getConfigurationFromFile();
1244 chargingStationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
1245 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
1246 (chargingStationConfiguration
?.connectorsStatus
|| chargingStationConfiguration
?.evsesStatus
)
1248 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration
);
1250 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
);
1252 this.stationInfo
= this.getStationInfo();
1254 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1255 isNotEmptyString(this.stationInfo
.firmwareVersion
) &&
1256 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
)
1258 const patternGroup
: number | undefined =
1259 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1260 this.stationInfo
.firmwareVersion
?.split('.').length
;
1261 const match
= this.stationInfo
1262 .firmwareVersion
!.match(new RegExp(this.stationInfo
.firmwareVersionPattern
!))!
1263 .slice(1, patternGroup
! + 1);
1264 const patchLevelIndex
= match
.length
- 1;
1265 match
[patchLevelIndex
] = (
1266 convertToInt(match
[patchLevelIndex
]) +
1267 this.stationInfo
.firmwareUpgrade
!.versionUpgrade
!.step
!
1269 this.stationInfo
.firmwareVersion
= match
?.join('.');
1271 this.saveStationInfo();
1272 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
1273 if (this.getEnableStatistics() === true) {
1274 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1275 this.stationInfo
.hashId
,
1276 this.stationInfo
.chargingStationId
!,
1277 this.configuredSupervisionUrl
,
1280 this.bootNotificationRequest
= createBootNotificationRequest(this.stationInfo
);
1281 this.powerDivider
= this.getPowerDivider();
1282 // OCPP configuration
1283 this.ocppConfiguration
= this.getOcppConfiguration();
1284 this.initializeOcppConfiguration();
1285 this.initializeOcppServices();
1286 if (this.stationInfo
?.autoRegister
=== true) {
1287 this.bootNotificationResponse
= {
1288 currentTime
: new Date(),
1289 interval
: millisecondsToSeconds(this.getHeartbeatInterval()),
1290 status: RegistrationStatusEnumType
.ACCEPTED
,
1295 private initializeOcppServices(): void {
1296 const ocppVersion
= this.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
1297 switch (ocppVersion
) {
1298 case OCPPVersion
.VERSION_16
:
1299 this.ocppIncomingRequestService
=
1300 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>();
1301 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1302 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>(),
1305 case OCPPVersion
.VERSION_20
:
1306 case OCPPVersion
.VERSION_201
:
1307 this.ocppIncomingRequestService
=
1308 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>();
1309 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1310 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>(),
1314 this.handleUnsupportedVersion(ocppVersion
);
1319 private initializeOcppConfiguration(): void {
1320 if (!getConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
)) {
1321 addConfigurationKey(this, StandardParametersKey
.HeartbeatInterval
, '0');
1323 if (!getConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
)) {
1324 addConfigurationKey(this, StandardParametersKey
.HeartBeatInterval
, '0', { visible
: false });
1327 this.getSupervisionUrlOcppConfiguration() &&
1328 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1329 !getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1331 addConfigurationKey(
1333 this.getSupervisionUrlOcppKey(),
1334 this.configuredSupervisionUrl
.href
,
1338 !this.getSupervisionUrlOcppConfiguration() &&
1339 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1340 getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1342 deleteConfigurationKey(this, this.getSupervisionUrlOcppKey(), { save
: false });
1345 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1346 !getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
!)
1348 addConfigurationKey(
1350 this.stationInfo
.amperageLimitationOcppKey
!,
1352 this.stationInfo
.maximumAmperage
! * getAmperageLimitationUnitDivider(this.stationInfo
)
1356 if (!getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
)) {
1357 addConfigurationKey(
1359 StandardParametersKey
.SupportedFeatureProfiles
,
1360 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`,
1363 addConfigurationKey(
1365 StandardParametersKey
.NumberOfConnectors
,
1366 this.getNumberOfConnectors().toString(),
1368 { overwrite
: true },
1370 if (!getConfigurationKey(this, StandardParametersKey
.MeterValuesSampledData
)) {
1371 addConfigurationKey(
1373 StandardParametersKey
.MeterValuesSampledData
,
1374 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
,
1377 if (!getConfigurationKey(this, StandardParametersKey
.ConnectorPhaseRotation
)) {
1378 const connectorsPhaseRotation
: string[] = [];
1379 if (this.hasEvses
) {
1380 for (const evseStatus
of this.evses
.values()) {
1381 for (const connectorId
of evseStatus
.connectors
.keys()) {
1382 connectorsPhaseRotation
.push(
1383 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!,
1388 for (const connectorId
of this.connectors
.keys()) {
1389 connectorsPhaseRotation
.push(
1390 getPhaseRotationValue(connectorId
, this.getNumberOfPhases())!,
1394 addConfigurationKey(
1396 StandardParametersKey
.ConnectorPhaseRotation
,
1397 connectorsPhaseRotation
.toString(),
1400 if (!getConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
)) {
1401 addConfigurationKey(this, StandardParametersKey
.AuthorizeRemoteTxRequests
, 'true');
1404 !getConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
) &&
1405 getConfigurationKey(this, StandardParametersKey
.SupportedFeatureProfiles
)?.value
?.includes(
1406 SupportedFeatureProfiles
.LocalAuthListManagement
,
1409 addConfigurationKey(this, StandardParametersKey
.LocalAuthListEnabled
, 'false');
1411 if (!getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)) {
1412 addConfigurationKey(
1414 StandardParametersKey
.ConnectionTimeOut
,
1415 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString(),
1418 this.saveOcppConfiguration();
1421 private initializeConnectorsOrEvsesFromFile(configuration
: ChargingStationConfiguration
): void {
1422 if (configuration
?.connectorsStatus
&& !configuration
?.evsesStatus
) {
1423 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1424 this.connectors
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
1426 } else if (configuration
?.evsesStatus
&& !configuration
?.connectorsStatus
) {
1427 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1428 const evseStatus
= cloneObject
<EvseStatusConfiguration
>(evseStatusConfiguration
);
1429 delete evseStatus
.connectorsStatus
;
1430 this.evses
.set(evseId
, {
1431 ...(evseStatus
as EvseStatus
),
1432 connectors
: new Map
<number, ConnectorStatus
>(
1433 evseStatusConfiguration
.connectorsStatus
!.map((connectorStatus
, connectorId
) => [
1440 } else if (configuration
?.evsesStatus
&& configuration
?.connectorsStatus
) {
1441 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1442 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1443 throw new BaseError(errorMsg
);
1445 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`;
1446 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1447 throw new BaseError(errorMsg
);
1451 private initializeConnectorsOrEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
) {
1452 if (stationTemplate
?.Connectors
&& !stationTemplate
?.Evses
) {
1453 this.initializeConnectorsFromTemplate(stationTemplate
);
1454 } else if (stationTemplate
?.Evses
&& !stationTemplate
?.Connectors
) {
1455 this.initializeEvsesFromTemplate(stationTemplate
);
1456 } else if (stationTemplate
?.Evses
&& stationTemplate
?.Connectors
) {
1457 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1458 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1459 throw new BaseError(errorMsg
);
1461 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`;
1462 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1463 throw new BaseError(errorMsg
);
1467 private initializeConnectorsFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1468 if (!stationTemplate
?.Connectors
&& this.connectors
.size
=== 0) {
1469 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1470 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1471 throw new BaseError(errorMsg
);
1473 if (!stationTemplate
?.Connectors
?.[0]) {
1475 `${this.logPrefix()} Charging station information from template ${
1477 } with no connector id 0 configuration`,
1480 if (stationTemplate
?.Connectors
) {
1481 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1482 checkConnectorsConfiguration(stationTemplate
, this.logPrefix(), this.templateFile
);
1483 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1485 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`,
1488 const connectorsConfigChanged
=
1489 this.connectors
?.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
;
1490 if (this.connectors
?.size
=== 0 || connectorsConfigChanged
) {
1491 connectorsConfigChanged
&& this.connectors
.clear();
1492 this.connectorsConfigurationHash
= connectorsConfigHash
;
1493 if (templateMaxConnectors
> 0) {
1494 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1496 connectorId
=== 0 &&
1497 (!stationTemplate
?.Connectors
?.[connectorId
] ||
1498 this.getUseConnectorId0(stationTemplate
) === false)
1502 const templateConnectorId
=
1503 connectorId
> 0 && stationTemplate
?.randomConnectors
1504 ? getRandomInteger(templateMaxAvailableConnectors
, 1)
1506 const connectorStatus
= stationTemplate
?.Connectors
[templateConnectorId
];
1507 checkStationInfoConnectorStatus(
1508 templateConnectorId
,
1513 this.connectors
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
1515 initializeConnectorsMapStatus(this.connectors
, this.logPrefix());
1516 this.saveConnectorsStatus();
1519 `${this.logPrefix()} Charging station information from template ${
1521 } with no connectors configuration defined, cannot create connectors`,
1527 `${this.logPrefix()} Charging station information from template ${
1529 } with no connectors configuration defined, using already defined connectors`,
1534 private initializeEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1535 if (!stationTemplate
?.Evses
&& this.evses
.size
=== 0) {
1536 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1537 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1538 throw new BaseError(errorMsg
);
1540 if (!stationTemplate
?.Evses
?.[0]) {
1542 `${this.logPrefix()} Charging station information from template ${
1544 } with no evse id 0 configuration`,
1547 if (!stationTemplate
?.Evses
?.[0]?.Connectors
?.[0]) {
1549 `${this.logPrefix()} Charging station information from template ${
1551 } with evse id 0 with no connector id 0 configuration`,
1554 if (Object.keys(stationTemplate
?.Evses
?.[0]?.Connectors
as object
).length
> 1) {
1556 `${this.logPrefix()} Charging station information from template ${
1558 } with evse id 0 with more than one connector configuration, only connector id 0 configuration will be used`,
1561 if (stationTemplate
?.Evses
) {
1562 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1563 .update(JSON
.stringify(stationTemplate
?.Evses
))
1565 const evsesConfigChanged
=
1566 this.evses
?.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
;
1567 if (this.evses
?.size
=== 0 || evsesConfigChanged
) {
1568 evsesConfigChanged
&& this.evses
.clear();
1569 this.evsesConfigurationHash
= evsesConfigHash
;
1570 const templateMaxEvses
= getMaxNumberOfEvses(stationTemplate
?.Evses
);
1571 if (templateMaxEvses
> 0) {
1572 for (const evseKey
in stationTemplate
.Evses
) {
1573 const evseId
= convertToInt(evseKey
);
1574 this.evses
.set(evseId
, {
1575 connectors
: buildConnectorsMap(
1576 stationTemplate
?.Evses
[evseKey
]?.Connectors
,
1580 availability
: AvailabilityType
.Operative
,
1582 initializeConnectorsMapStatus(this.evses
.get(evseId
)!.connectors
, this.logPrefix());
1584 this.saveEvsesStatus();
1587 `${this.logPrefix()} Charging station information from template ${
1589 } with no evses configuration defined, cannot create evses`,
1595 `${this.logPrefix()} Charging station information from template ${
1597 } with no evses configuration defined, using already defined evses`,
1602 private getConfigurationFromFile(): ChargingStationConfiguration
| undefined {
1603 let configuration
: ChargingStationConfiguration
| undefined;
1604 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1606 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1607 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1608 this.configurationFileHash
,
1611 const measureId
= `${FileType.ChargingStationConfiguration} read`;
1612 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1613 configuration
= JSON
.parse(
1614 readFileSync(this.configurationFile
, 'utf8'),
1615 ) as ChargingStationConfiguration
;
1616 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1617 this.sharedLRUCache
.setChargingStationConfiguration(configuration
);
1618 this.configurationFileHash
= configuration
.configurationHash
!;
1621 handleFileException(
1622 this.configurationFile
,
1623 FileType
.ChargingStationConfiguration
,
1624 error
as NodeJS
.ErrnoException
,
1629 return configuration
;
1632 private saveAutomaticTransactionGeneratorConfiguration(): void {
1633 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1634 this.saveConfiguration();
1638 private saveConnectorsStatus() {
1639 this.saveConfiguration();
1642 private saveEvsesStatus() {
1643 this.saveConfiguration();
1646 private saveConfiguration(): void {
1647 if (isNotEmptyString(this.configurationFile
)) {
1649 if (!existsSync(dirname(this.configurationFile
))) {
1650 mkdirSync(dirname(this.configurationFile
), { recursive
: true });
1652 let configurationData
: ChargingStationConfiguration
= this.getConfigurationFromFile()
1653 ? cloneObject
<ChargingStationConfiguration
>(this.getConfigurationFromFile()!)
1655 if (this.getStationInfoPersistentConfiguration() && this.stationInfo
) {
1656 configurationData
.stationInfo
= this.stationInfo
;
1658 delete configurationData
.stationInfo
;
1660 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration
?.configurationKey
) {
1661 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
;
1663 delete configurationData
.configurationKey
;
1665 configurationData
= merge
<ChargingStationConfiguration
>(
1667 buildChargingStationAutomaticTransactionGeneratorConfiguration(this),
1670 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1671 !this.getAutomaticTransactionGeneratorConfiguration()
1673 delete configurationData
.automaticTransactionGenerator
;
1675 if (this.connectors
.size
> 0) {
1676 configurationData
.connectorsStatus
= buildConnectorsStatus(this);
1678 delete configurationData
.connectorsStatus
;
1680 if (this.evses
.size
> 0) {
1681 configurationData
.evsesStatus
= buildEvsesStatus(this);
1683 delete configurationData
.evsesStatus
;
1685 delete configurationData
.configurationHash
;
1686 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1689 stationInfo
: configurationData
.stationInfo
,
1690 configurationKey
: configurationData
.configurationKey
,
1691 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1692 } as ChargingStationConfiguration
),
1695 if (this.configurationFileHash
!== configurationHash
) {
1696 AsyncLock
.runExclusive(AsyncLockType
.configuration
, () => {
1697 configurationData
.configurationHash
= configurationHash
;
1698 const measureId
= `${FileType.ChargingStationConfiguration} write`;
1699 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1701 this.configurationFile
,
1702 JSON
.stringify(configurationData
, null, 2),
1705 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1706 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
1707 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
);
1708 this.configurationFileHash
= configurationHash
;
1709 }).catch((error
) => {
1710 handleFileException(
1711 this.configurationFile
,
1712 FileType
.ChargingStationConfiguration
,
1713 error
as NodeJS
.ErrnoException
,
1719 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1720 this.configurationFile
1725 handleFileException(
1726 this.configurationFile
,
1727 FileType
.ChargingStationConfiguration
,
1728 error
as NodeJS
.ErrnoException
,
1734 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`,
1739 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration
| undefined {
1740 return this.getTemplateFromFile()?.Configuration
;
1743 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration
| undefined {
1744 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
;
1745 if (this.getOcppPersistentConfiguration() === true && configurationKey
) {
1746 return { configurationKey
};
1751 private getOcppConfiguration(): ChargingStationOcppConfiguration
| undefined {
1752 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1753 this.getOcppConfigurationFromFile();
1754 if (!ocppConfiguration
) {
1755 ocppConfiguration
= this.getOcppConfigurationFromTemplate();
1757 return ocppConfiguration
;
1760 private async onOpen(): Promise
<void> {
1761 if (this.isWebSocketConnectionOpened() === true) {
1763 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`,
1765 if (this.isRegistered() === false) {
1766 // Send BootNotification
1767 let registrationRetryCount
= 0;
1769 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1770 BootNotificationRequest
,
1771 BootNotificationResponse
1772 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1773 skipBufferingOnError
: true,
1775 if (this.isRegistered() === false) {
1776 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount
;
1778 this?.bootNotificationResponse
?.interval
1779 ? secondsToMilliseconds(this.bootNotificationResponse
.interval
)
1780 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
,
1784 this.isRegistered() === false &&
1785 (registrationRetryCount
<= this.getRegistrationMaxRetries()! ||
1786 this.getRegistrationMaxRetries() === -1)
1789 if (this.isRegistered() === true) {
1790 if (this.inAcceptedState() === true) {
1791 await this.startMessageSequence();
1795 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`,
1798 this.wsConnectionRestarted
= false;
1799 this.autoReconnectRetryCount
= 0;
1800 parentPort
?.postMessage(buildUpdatedMessage(this));
1803 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
1808 private async onClose(code
: number, reason
: Buffer
): Promise
<void> {
1811 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1812 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1814 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1816 )}' and reason '${reason.toString()}'`,
1818 this.autoReconnectRetryCount
= 0;
1823 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1825 )}' and reason '${reason.toString()}'`,
1827 this.started
=== true && (await this.reconnect());
1830 parentPort
?.postMessage(buildUpdatedMessage(this));
1833 private getCachedRequest(messageType
: MessageType
, messageId
: string): CachedRequest
| undefined {
1834 const cachedRequest
= this.requests
.get(messageId
);
1835 if (Array.isArray(cachedRequest
) === true) {
1836 return cachedRequest
;
1838 throw new OCPPError(
1839 ErrorType
.PROTOCOL_ERROR
,
1840 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1842 )} is not an array`,
1844 cachedRequest
as JsonType
,
1848 private async handleIncomingMessage(request
: IncomingRequest
): Promise
<void> {
1849 const [messageType
, messageId
, commandName
, commandPayload
] = request
;
1850 if (this.getEnableStatistics() === true) {
1851 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
1854 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1858 // Process the message
1859 await this.ocppIncomingRequestService
.incomingRequestHandler(
1867 private handleResponseMessage(response
: Response
): void {
1868 const [messageType
, messageId
, commandPayload
] = response
;
1869 if (this.requests
.has(messageId
) === false) {
1871 throw new OCPPError(
1872 ErrorType
.INTERNAL_ERROR
,
1873 `Response for unknown message id ${messageId}`,
1879 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1884 `${this.logPrefix()} << Command '${
1885 requestCommandName ?? Constants.UNKNOWN_COMMAND
1886 }' received response payload: ${JSON.stringify(response)}`,
1888 responseCallback(commandPayload
, requestPayload
);
1891 private handleErrorMessage(errorResponse
: ErrorResponse
): void {
1892 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
;
1893 if (this.requests
.has(messageId
) === false) {
1895 throw new OCPPError(
1896 ErrorType
.INTERNAL_ERROR
,
1897 `Error response for unknown message id ${messageId}`,
1899 { errorType
, errorMessage
, errorDetails
},
1902 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!;
1904 `${this.logPrefix()} << Command '${
1905 requestCommandName ?? Constants.UNKNOWN_COMMAND
1906 }' received error response payload: ${JSON.stringify(errorResponse)}`,
1908 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
));
1911 private async onMessage(data
: RawData
): Promise
<void> {
1912 let request
: IncomingRequest
| Response
| ErrorResponse
| undefined;
1913 let messageType
: number | undefined;
1914 let errorMsg
: string;
1916 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1917 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
;
1918 if (Array.isArray(request
) === true) {
1919 [messageType
] = request
;
1920 // Check the type of message
1921 switch (messageType
) {
1923 case MessageType
.CALL_MESSAGE
:
1924 await this.handleIncomingMessage(request
as IncomingRequest
);
1927 case MessageType
.CALL_RESULT_MESSAGE
:
1928 this.handleResponseMessage(request
as Response
);
1931 case MessageType
.CALL_ERROR_MESSAGE
:
1932 this.handleErrorMessage(request
as ErrorResponse
);
1936 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
1937 errorMsg
= `Wrong message type ${messageType}`;
1938 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1939 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
);
1941 parentPort
?.postMessage(buildUpdatedMessage(this));
1943 throw new OCPPError(
1944 ErrorType
.PROTOCOL_ERROR
,
1945 'Incoming message is not an array',
1953 let commandName
: IncomingRequestCommand
| undefined;
1954 let requestCommandName
: RequestCommand
| IncomingRequestCommand
| undefined;
1955 let errorCallback
: ErrorCallback
;
1956 const [, messageId
] = request
!;
1957 switch (messageType
) {
1958 case MessageType
.CALL_MESSAGE
:
1959 [, , commandName
] = request
as IncomingRequest
;
1961 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
);
1963 case MessageType
.CALL_RESULT_MESSAGE
:
1964 case MessageType
.CALL_ERROR_MESSAGE
:
1965 if (this.requests
.has(messageId
) === true) {
1966 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
)!;
1967 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
1968 errorCallback(error
as OCPPError
, false);
1970 // Remove the request from the cache in case of error at response handling
1971 this.requests
.delete(messageId
);
1975 if (error
instanceof OCPPError
=== false) {
1977 `${this.logPrefix()} Error thrown at incoming OCPP command '${
1978 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1979 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1980 }' message '${data.toString()}' handling is not an OCPPError:`,
1985 `${this.logPrefix()} Incoming OCPP command '${
1986 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
1987 // eslint-disable-next-line @typescript-eslint/no-base-to-string
1988 }' message '${data.toString()}'${
1989 messageType !== MessageType.CALL_MESSAGE
1990 ? ` matching cached request
'${JSON.stringify(this.requests.get(messageId))}'`
1992 } processing error:`,
1998 private onPing(): void {
1999 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2002 private onPong(): void {
2003 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2006 private onError(error
: WSError
): void {
2007 this.closeWSConnection();
2008 logger
.error(`${this.logPrefix()} WebSocket error:`, error
);
2011 private getEnergyActiveImportRegister(connectorStatus
: ConnectorStatus
, rounded
= false): number {
2012 if (this.getMeteringPerTransaction() === true) {
2015 ? Math.round(connectorStatus
.transactionEnergyActiveImportRegisterValue
!)
2016 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2021 ? Math.round(connectorStatus
.energyActiveImportRegisterValue
!)
2022 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2026 private getUseConnectorId0(stationTemplate
?: ChargingStationTemplate
): boolean {
2027 return stationTemplate
?.useConnectorId0
?? true;
2030 private async stopRunningTransactions(reason
= StopTransactionReason
.NONE
): Promise
<void> {
2031 if (this.hasEvses
) {
2032 for (const [evseId
, evseStatus
] of this.evses
) {
2036 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2037 if (connectorStatus
.transactionStarted
=== true) {
2038 await this.stopTransactionOnConnector(connectorId
, reason
);
2043 for (const connectorId
of this.connectors
.keys()) {
2044 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2045 await this.stopTransactionOnConnector(connectorId
, reason
);
2052 private getConnectionTimeout(): number {
2053 if (getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)) {
2055 parseInt(getConfigurationKey(this, StandardParametersKey
.ConnectionTimeOut
)!.value
!) ??
2056 Constants
.DEFAULT_CONNECTION_TIMEOUT
2059 return Constants
.DEFAULT_CONNECTION_TIMEOUT
;
2062 // -1 for unlimited, 0 for disabling
2063 private getAutoReconnectMaxRetries(): number | undefined {
2064 return this.stationInfo
.autoReconnectMaxRetries
?? -1;
2067 // -1 for unlimited, 0 for disabling
2068 private getRegistrationMaxRetries(): number | undefined {
2069 return this.stationInfo
.registrationMaxRetries
?? -1;
2072 private getPowerDivider(): number {
2073 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors();
2074 if (this.stationInfo
?.powerSharedByConnectors
) {
2075 powerDivider
= this.getNumberOfRunningTransactions();
2077 return powerDivider
;
2080 private getMaximumAmperage(stationInfo
: ChargingStationInfo
): number | undefined {
2081 const maximumPower
= this.getMaximumPower(stationInfo
);
2082 switch (this.getCurrentOutType(stationInfo
)) {
2083 case CurrentType
.AC
:
2084 return ACElectricUtils
.amperagePerPhaseFromPower(
2085 this.getNumberOfPhases(stationInfo
),
2086 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2087 this.getVoltageOut(stationInfo
),
2089 case CurrentType
.DC
:
2090 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
));
2094 private getAmperageLimitation(): number | undefined {
2096 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2097 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
!)
2101 getConfigurationKey(this, this.stationInfo
.amperageLimitationOcppKey
!)?.value
,
2102 ) / getAmperageLimitationUnitDivider(this.stationInfo
)
2107 private async startMessageSequence(): Promise
<void> {
2108 if (this.stationInfo
?.autoRegister
=== true) {
2109 await this.ocppRequestService
.requestHandler
<
2110 BootNotificationRequest
,
2111 BootNotificationResponse
2112 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2113 skipBufferingOnError
: true,
2116 // Start WebSocket ping
2117 this.startWebSocketPing();
2119 this.startHeartbeat();
2120 // Initialize connectors status
2121 if (this.hasEvses
) {
2122 for (const [evseId
, evseStatus
] of this.evses
) {
2124 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2125 const connectorBootStatus
= getBootConnectorStatus(this, connectorId
, connectorStatus
);
2126 await OCPPServiceUtils
.sendAndSetConnectorStatus(
2129 connectorBootStatus
,
2136 for (const connectorId
of this.connectors
.keys()) {
2137 if (connectorId
> 0) {
2138 const connectorBootStatus
= getBootConnectorStatus(
2141 this.getConnectorStatus(connectorId
)!,
2143 await OCPPServiceUtils
.sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
);
2147 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2148 await this.ocppRequestService
.requestHandler
<
2149 FirmwareStatusNotificationRequest
,
2150 FirmwareStatusNotificationResponse
2151 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2152 status: FirmwareStatus
.Installed
,
2154 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
;
2158 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2159 this.startAutomaticTransactionGenerator();
2161 this.wsConnectionRestarted
=== true && this.flushMessageBuffer();
2164 private async stopMessageSequence(
2165 reason
: StopTransactionReason
= StopTransactionReason
.NONE
,
2167 // Stop WebSocket ping
2168 this.stopWebSocketPing();
2170 this.stopHeartbeat();
2171 // Stop ongoing transactions
2172 if (this.automaticTransactionGenerator
?.started
=== true) {
2173 this.stopAutomaticTransactionGenerator();
2175 await this.stopRunningTransactions(reason
);
2177 if (this.hasEvses
) {
2178 for (const [evseId
, evseStatus
] of this.evses
) {
2180 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2181 await this.ocppRequestService
.requestHandler
<
2182 StatusNotificationRequest
,
2183 StatusNotificationResponse
2186 RequestCommand
.STATUS_NOTIFICATION
,
2187 OCPPServiceUtils
.buildStatusNotificationRequest(
2190 ConnectorStatusEnum
.Unavailable
,
2194 delete connectorStatus
?.status;
2199 for (const connectorId
of this.connectors
.keys()) {
2200 if (connectorId
> 0) {
2201 await this.ocppRequestService
.requestHandler
<
2202 StatusNotificationRequest
,
2203 StatusNotificationResponse
2206 RequestCommand
.STATUS_NOTIFICATION
,
2207 OCPPServiceUtils
.buildStatusNotificationRequest(
2210 ConnectorStatusEnum
.Unavailable
,
2213 delete this.getConnectorStatus(connectorId
)?.status;
2219 private startWebSocketPing(): void {
2220 const webSocketPingInterval
: number = getConfigurationKey(
2222 StandardParametersKey
.WebSocketPingInterval
,
2224 ? convertToInt(getConfigurationKey(this, StandardParametersKey
.WebSocketPingInterval
)?.value
)
2226 if (webSocketPingInterval
> 0 && !this.webSocketPingSetInterval
) {
2227 this.webSocketPingSetInterval
= setInterval(() => {
2228 if (this.isWebSocketConnectionOpened() === true) {
2229 this.wsConnection
?.ping();
2231 }, secondsToMilliseconds(webSocketPingInterval
));
2233 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2234 webSocketPingInterval,
2237 } else if (this.webSocketPingSetInterval
) {
2239 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2240 webSocketPingInterval,
2245 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`,
2250 private stopWebSocketPing(): void {
2251 if (this.webSocketPingSetInterval
) {
2252 clearInterval(this.webSocketPingSetInterval
);
2253 delete this.webSocketPingSetInterval
;
2257 private getConfiguredSupervisionUrl(): URL
{
2258 let configuredSupervisionUrl
: string;
2259 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls();
2260 if (isNotEmptyArray(supervisionUrls
)) {
2261 let configuredSupervisionUrlIndex
: number;
2262 switch (Configuration
.getSupervisionUrlDistribution()) {
2263 case SupervisionUrlDistribution
.RANDOM
:
2264 configuredSupervisionUrlIndex
= Math.floor(
2265 secureRandom() * (supervisionUrls
as string[]).length
,
2268 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2269 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2271 Object.values(SupervisionUrlDistribution
).includes(
2272 Configuration
.getSupervisionUrlDistribution()!,
2275 // eslint-disable-next-line @typescript-eslint/no-base-to-string
2276 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2277 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2280 configuredSupervisionUrlIndex
= (this.index
- 1) % (supervisionUrls
as string[]).length
;
2283 configuredSupervisionUrl
= (supervisionUrls
as string[])[configuredSupervisionUrlIndex
];
2285 configuredSupervisionUrl
= supervisionUrls
as string;
2287 if (isNotEmptyString(configuredSupervisionUrl
)) {
2288 return new URL(configuredSupervisionUrl
);
2290 const errorMsg
= 'No supervision url(s) configured';
2291 logger
.error(`${this.logPrefix()} ${errorMsg}`);
2292 throw new BaseError(`${errorMsg}`);
2295 private stopHeartbeat(): void {
2296 if (this.heartbeatSetInterval
) {
2297 clearInterval(this.heartbeatSetInterval
);
2298 delete this.heartbeatSetInterval
;
2302 private terminateWSConnection(): void {
2303 if (this.isWebSocketConnectionOpened() === true) {
2304 this.wsConnection
?.terminate();
2305 this.wsConnection
= null;
2309 private getReconnectExponentialDelay(): boolean {
2310 return this.stationInfo
?.reconnectExponentialDelay
?? false;
2313 private async reconnect(): Promise
<void> {
2314 // Stop WebSocket ping
2315 this.stopWebSocketPing();
2317 this.stopHeartbeat();
2318 // Stop the ATG if needed
2319 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure
=== true) {
2320 this.stopAutomaticTransactionGenerator();
2323 this.autoReconnectRetryCount
< this.getAutoReconnectMaxRetries()! ||
2324 this.getAutoReconnectMaxRetries() === -1
2326 ++this.autoReconnectRetryCount
;
2327 const reconnectDelay
= this.getReconnectExponentialDelay()
2328 ? exponentialDelay(this.autoReconnectRetryCount
)
2329 : secondsToMilliseconds(this.getConnectionTimeout());
2330 const reconnectDelayWithdraw
= 1000;
2331 const reconnectTimeout
=
2332 reconnectDelay
&& reconnectDelay
- reconnectDelayWithdraw
> 0
2333 ? reconnectDelay
- reconnectDelayWithdraw
2336 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2339 )}ms, timeout ${reconnectTimeout}ms`,
2341 await sleep(reconnectDelay
);
2343 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`,
2345 this.openWSConnection(
2347 handshakeTimeout
: reconnectTimeout
,
2349 { closeOpened
: true },
2351 this.wsConnectionRestarted
= true;
2352 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2354 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2355 this.autoReconnectRetryCount
2356 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`,