1 // Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
3 import { createHash
} from
'node:crypto';
13 import { dirname
, join
} from
'node:path';
14 import { URL
} from
'node:url';
15 import { parentPort
} from
'node:worker_threads';
17 import merge from
'just-merge';
18 import { type RawData
, WebSocket
} from
'ws';
20 import { AutomaticTransactionGenerator
} from
'./AutomaticTransactionGenerator';
21 import { ChargingStationWorkerBroadcastChannel
} from
'./broadcast-channel/ChargingStationWorkerBroadcastChannel';
22 import { ChargingStationConfigurationUtils
} from
'./ChargingStationConfigurationUtils';
23 import { ChargingStationUtils
} from
'./ChargingStationUtils';
24 import { IdTagsCache
} from
'./IdTagsCache';
26 OCPP16IncomingRequestService
,
28 OCPP16ResponseService
,
30 OCPP20IncomingRequestService
,
32 OCPP20ResponseService
,
33 type OCPPIncomingRequestService
,
34 type OCPPRequestService
,
37 import { SharedLRUCache
} from
'./SharedLRUCache';
38 import { BaseError
, OCPPError
} from
'../exception';
39 import { PerformanceStatistics
} from
'../performance';
41 type AutomaticTransactionGeneratorConfiguration
,
43 type BootNotificationRequest
,
44 type BootNotificationResponse
,
46 type ChargingStationConfiguration
,
47 type ChargingStationInfo
,
48 type ChargingStationOcppConfiguration
,
49 type ChargingStationTemplate
,
57 type EvseStatusConfiguration
,
60 type FirmwareStatusNotificationRequest
,
61 type FirmwareStatusNotificationResponse
,
63 type HeartbeatRequest
,
64 type HeartbeatResponse
,
66 type IncomingRequestCommand
,
71 type MeterValuesRequest
,
72 type MeterValuesResponse
,
76 RegistrationStatusEnumType
,
80 ReservationTerminationReason
,
82 StandardParametersKey
,
84 type StatusNotificationRequest
,
85 type StatusNotificationResponse
,
86 StopTransactionReason
,
87 type StopTransactionRequest
,
88 type StopTransactionResponse
,
89 SupervisionUrlDistribution
,
90 SupportedFeatureProfiles
,
93 WebSocketCloseEventStatusCode
,
104 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
105 buildConnectorsStatus
,
115 export class ChargingStation
{
116 public readonly index
: number;
117 public readonly templateFile
: string;
118 public stationInfo
!: ChargingStationInfo
;
119 public started
: boolean;
120 public starting
: boolean;
121 public idTagsCache
: IdTagsCache
;
122 public automaticTransactionGenerator
!: AutomaticTransactionGenerator
| undefined;
123 public ocppConfiguration
!: ChargingStationOcppConfiguration
| undefined;
124 public wsConnection
!: WebSocket
| null;
125 public readonly connectors
: Map
<number, ConnectorStatus
>;
126 public readonly evses
: Map
<number, EvseStatus
>;
127 public readonly requests
: Map
<string, CachedRequest
>;
128 public performanceStatistics
!: PerformanceStatistics
| undefined;
129 public heartbeatSetInterval
!: NodeJS
.Timeout
;
130 public ocppRequestService
!: OCPPRequestService
;
131 public bootNotificationRequest
!: BootNotificationRequest
;
132 public bootNotificationResponse
!: BootNotificationResponse
| undefined;
133 public powerDivider
!: number;
134 private stopping
: boolean;
135 private configurationFile
!: string;
136 private configurationFileHash
!: string;
137 private connectorsConfigurationHash
!: string;
138 private evsesConfigurationHash
!: string;
139 private ocppIncomingRequestService
!: OCPPIncomingRequestService
;
140 private readonly messageBuffer
: Set
<string>;
141 private configuredSupervisionUrl
!: URL
;
142 private wsConnectionRestarted
: boolean;
143 private autoReconnectRetryCount
: number;
144 private templateFileWatcher
!: FSWatcher
| undefined;
145 private templateFileHash
!: string;
146 private readonly sharedLRUCache
: SharedLRUCache
;
147 private webSocketPingSetInterval
!: NodeJS
.Timeout
;
148 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
;
149 private reservationExpirationSetInterval
?: NodeJS
.Timeout
;
151 constructor(index
: number, templateFile
: string) {
152 this.started
= false;
153 this.starting
= false;
154 this.stopping
= false;
155 this.wsConnectionRestarted
= false;
156 this.autoReconnectRetryCount
= 0;
158 this.templateFile
= templateFile
;
159 this.connectors
= new Map
<number, ConnectorStatus
>();
160 this.evses
= new Map
<number, EvseStatus
>();
161 this.requests
= new Map
<string, CachedRequest
>();
162 this.messageBuffer
= new Set
<string>();
163 this.sharedLRUCache
= SharedLRUCache
.getInstance();
164 this.idTagsCache
= IdTagsCache
.getInstance();
165 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this);
170 public get
hasEvses(): boolean {
171 return this.connectors
.size
=== 0 && this.evses
.size
> 0;
174 private get
wsConnectionUrl(): URL
{
177 this.getSupervisionUrlOcppConfiguration() &&
178 Utils.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
179 Utils.isNotEmptyString(
180 ChargingStationConfigurationUtils.getConfigurationKey(
182 this.getSupervisionUrlOcppKey()
185 ? ChargingStationConfigurationUtils.getConfigurationKey(
187 this.getSupervisionUrlOcppKey()
189 : this.configuredSupervisionUrl.href
190 }/${this.stationInfo.chargingStationId}`
194 public logPrefix
= (): string => {
195 return Utils
.logPrefix(
197 (Utils.isNotEmptyString(this?.stationInfo?.chargingStationId)
198 ? this?.stationInfo?.chargingStationId
199 : ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())) ??
200 'Error at building log prefix'
205 public hasIdTags(): boolean {
206 return Utils
.isNotEmptyArray(
207 this.idTagsCache
.getIdTags(ChargingStationUtils
.getIdTagsFile(this.stationInfo
))
211 public getEnableStatistics(): boolean {
212 return this.stationInfo
.enableStatistics
?? false;
215 public getMustAuthorizeAtRemoteStart(): boolean {
216 return this.stationInfo
.mustAuthorizeAtRemoteStart
?? true;
219 public getPayloadSchemaValidation(): boolean {
220 return this.stationInfo
.payloadSchemaValidation
?? true;
223 public getNumberOfPhases(stationInfo
?: ChargingStationInfo
): number | undefined {
224 const localStationInfo
: ChargingStationInfo
= stationInfo
?? this.stationInfo
;
225 switch (this.getCurrentOutType(stationInfo
)) {
227 return !Utils
.isUndefined(localStationInfo
.numberOfPhases
)
228 ? localStationInfo
.numberOfPhases
235 public isWebSocketConnectionOpened(): boolean {
236 return this?.wsConnection
?.readyState
=== WebSocket
.OPEN
;
239 public getRegistrationStatus(): RegistrationStatusEnumType
| undefined {
240 return this?.bootNotificationResponse
?.status;
243 public inUnknownState(): boolean {
244 return Utils
.isNullOrUndefined(this?.bootNotificationResponse
?.status);
247 public inPendingState(): boolean {
248 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
;
251 public inAcceptedState(): boolean {
252 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
;
255 public inRejectedState(): boolean {
256 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
;
259 public isRegistered(): boolean {
261 this.inUnknownState() === false &&
262 (this.inAcceptedState() === true || this.inPendingState() === true)
266 public isChargingStationAvailable(): boolean {
267 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
;
270 public hasConnector(connectorId
: number): boolean {
272 for (const evseStatus
of this.evses
.values()) {
273 if (evseStatus
.connectors
.has(connectorId
)) {
279 return this.connectors
.has(connectorId
);
282 public isConnectorAvailable(connectorId
: number): boolean {
285 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
289 public getNumberOfConnectors(): number {
291 let numberOfConnectors
= 0;
292 for (const [evseId
, evseStatus
] of this.evses
) {
294 numberOfConnectors
+= evseStatus
.connectors
.size
;
297 return numberOfConnectors
;
299 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
;
302 public getNumberOfEvses(): number {
303 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
;
306 public getConnectorStatus(connectorId
: number): ConnectorStatus
| undefined {
308 for (const evseStatus
of this.evses
.values()) {
309 if (evseStatus
.connectors
.has(connectorId
)) {
310 return evseStatus
.connectors
.get(connectorId
);
315 return this.connectors
.get(connectorId
);
318 public getCurrentOutType(stationInfo
?: ChargingStationInfo
): CurrentType
{
319 return (stationInfo
?? this.stationInfo
)?.currentOutType
?? CurrentType
.AC
;
322 public getOcppStrictCompliance(): boolean {
323 return this.stationInfo
?.ocppStrictCompliance
?? false;
326 public getVoltageOut(stationInfo
?: ChargingStationInfo
): number | undefined {
327 const defaultVoltageOut
= ChargingStationUtils
.getDefaultVoltageOut(
328 this.getCurrentOutType(stationInfo
),
332 return (stationInfo
?? this.stationInfo
).voltageOut
?? defaultVoltageOut
;
335 public getMaximumPower(stationInfo
?: ChargingStationInfo
): number {
336 const localStationInfo
= stationInfo
?? this.stationInfo
;
337 return (localStationInfo
['maxPower'] as number) ?? localStationInfo
.maximumPower
;
340 public getConnectorMaximumAvailablePower(connectorId
: number): number {
341 let connectorAmperageLimitationPowerLimit
: number;
343 !Utils
.isNullOrUndefined(this.getAmperageLimitation()) &&
344 this.getAmperageLimitation() < this.stationInfo
?.maximumAmperage
346 connectorAmperageLimitationPowerLimit
=
347 (this.getCurrentOutType() === CurrentType
.AC
348 ? ACElectricUtils
.powerTotal(
349 this.getNumberOfPhases(),
350 this.getVoltageOut(),
351 this.getAmperageLimitation() *
352 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
354 : DCElectricUtils
.power(this.getVoltageOut(), this.getAmperageLimitation())) /
357 const connectorMaximumPower
= this.getMaximumPower() / this.powerDivider
;
358 const connectorChargingProfilesPowerLimit
=
359 ChargingStationUtils
.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
);
361 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
362 isNaN(connectorAmperageLimitationPowerLimit
)
364 : connectorAmperageLimitationPowerLimit
,
365 isNaN(connectorChargingProfilesPowerLimit
) ? Infinity : connectorChargingProfilesPowerLimit
369 public getTransactionIdTag(transactionId
: number): string | undefined {
371 for (const evseStatus
of this.evses
.values()) {
372 for (const connectorStatus
of evseStatus
.connectors
.values()) {
373 if (connectorStatus
.transactionId
=== transactionId
) {
374 return connectorStatus
.transactionIdTag
;
379 for (const connectorId
of this.connectors
.keys()) {
380 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
381 return this.getConnectorStatus(connectorId
)?.transactionIdTag
;
387 public getNumberOfRunningTransactions(): number {
390 for (const [evseId
, evseStatus
] of this.evses
) {
394 for (const connectorStatus
of evseStatus
.connectors
.values()) {
395 if (connectorStatus
.transactionStarted
=== true) {
401 for (const connectorId
of this.connectors
.keys()) {
402 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
410 public getOutOfOrderEndMeterValues(): boolean {
411 return this.stationInfo
?.outOfOrderEndMeterValues
?? false;
414 public getBeginEndMeterValues(): boolean {
415 return this.stationInfo
?.beginEndMeterValues
?? false;
418 public getMeteringPerTransaction(): boolean {
419 return this.stationInfo
?.meteringPerTransaction
?? true;
422 public getTransactionDataMeterValues(): boolean {
423 return this.stationInfo
?.transactionDataMeterValues
?? false;
426 public getMainVoltageMeterValues(): boolean {
427 return this.stationInfo
?.mainVoltageMeterValues
?? true;
430 public getPhaseLineToLineVoltageMeterValues(): boolean {
431 return this.stationInfo
?.phaseLineToLineVoltageMeterValues
?? false;
434 public getCustomValueLimitationMeterValues(): boolean {
435 return this.stationInfo
?.customValueLimitationMeterValues
?? true;
438 public getConnectorIdByTransactionId(transactionId
: number): number | undefined {
440 for (const evseStatus
of this.evses
.values()) {
441 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
442 if (connectorStatus
.transactionId
=== transactionId
) {
448 for (const connectorId
of this.connectors
.keys()) {
449 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
456 public getEnergyActiveImportRegisterByTransactionId(
457 transactionId
: number,
460 return this.getEnergyActiveImportRegister(
461 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)),
466 public getEnergyActiveImportRegisterByConnectorId(connectorId
: number, rounded
= false): number {
467 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
);
470 public getAuthorizeRemoteTxRequests(): boolean {
471 const authorizeRemoteTxRequests
= ChargingStationConfigurationUtils
.getConfigurationKey(
473 StandardParametersKey
.AuthorizeRemoteTxRequests
475 return authorizeRemoteTxRequests
476 ? Utils
.convertToBoolean(authorizeRemoteTxRequests
.value
)
480 public getLocalAuthListEnabled(): boolean {
481 const localAuthListEnabled
= ChargingStationConfigurationUtils
.getConfigurationKey(
483 StandardParametersKey
.LocalAuthListEnabled
485 return localAuthListEnabled
? Utils
.convertToBoolean(localAuthListEnabled
.value
) : false;
488 public getHeartbeatInterval(): number {
489 const HeartbeatInterval
= ChargingStationConfigurationUtils
.getConfigurationKey(
491 StandardParametersKey
.HeartbeatInterval
493 if (HeartbeatInterval
) {
494 return Utils
.convertToInt(HeartbeatInterval
.value
) * 1000;
496 const HeartBeatInterval
= ChargingStationConfigurationUtils
.getConfigurationKey(
498 StandardParametersKey
.HeartBeatInterval
500 if (HeartBeatInterval
) {
501 return Utils
.convertToInt(HeartBeatInterval
.value
) * 1000;
503 this.stationInfo
?.autoRegister
=== false &&
505 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
506 Constants.DEFAULT_HEARTBEAT_INTERVAL
509 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
;
512 public setSupervisionUrl(url
: string): void {
514 this.getSupervisionUrlOcppConfiguration() &&
515 Utils
.isNotEmptyString(this.getSupervisionUrlOcppKey())
517 ChargingStationConfigurationUtils
.setConfigurationKeyValue(
519 this.getSupervisionUrlOcppKey(),
523 this.stationInfo
.supervisionUrls
= url
;
524 this.saveStationInfo();
525 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
529 public startHeartbeat(): void {
530 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval
) {
531 this.heartbeatSetInterval
= setInterval(() => {
532 this.ocppRequestService
533 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
536 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
540 }, this.getHeartbeatInterval());
542 `${this.logPrefix()} Heartbeat started every ${Utils.formatDurationMilliSeconds(
543 this.getHeartbeatInterval()
546 } else if (this.heartbeatSetInterval
) {
548 `${this.logPrefix()} Heartbeat already started every ${Utils.formatDurationMilliSeconds(
549 this.getHeartbeatInterval()
554 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
555 not starting the heartbeat`
560 public restartHeartbeat(): void {
562 this.stopHeartbeat();
564 this.startHeartbeat();
567 public restartWebSocketPing(): void {
568 // Stop WebSocket ping
569 this.stopWebSocketPing();
570 // Start WebSocket ping
571 this.startWebSocketPing();
574 public startMeterValues(connectorId
: number, interval
: number): void {
575 if (connectorId
=== 0) {
577 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId.toString()}`
581 if (!this.getConnectorStatus(connectorId
)) {
583 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
584 ${connectorId.toString()}`
588 if (this.getConnectorStatus(connectorId
)?.transactionStarted
=== false) {
590 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
591 with no transaction started`
595 this.getConnectorStatus(connectorId
)?.transactionStarted
=== true &&
596 Utils
.isNullOrUndefined(this.getConnectorStatus(connectorId
)?.transactionId
)
599 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
600 with no transaction id`
605 this.getConnectorStatus(connectorId
).transactionSetInterval
= setInterval(() => {
606 // FIXME: Implement OCPP version agnostic helpers
607 const meterValue
: MeterValue
= OCPP16ServiceUtils
.buildMeterValue(
610 this.getConnectorStatus(connectorId
).transactionId
,
613 this.ocppRequestService
614 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
616 RequestCommand
.METER_VALUES
,
619 transactionId
: this.getConnectorStatus(connectorId
)?.transactionId
,
620 meterValue
: [meterValue
],
625 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
632 `${this.logPrefix()} Charging station ${
633 StandardParametersKey.MeterValueSampleInterval
634 } configuration set to ${interval}, not sending MeterValues`
639 public stopMeterValues(connectorId
: number) {
640 if (this.getConnectorStatus(connectorId
)?.transactionSetInterval
) {
641 clearInterval(this.getConnectorStatus(connectorId
)?.transactionSetInterval
);
645 public start(): void {
646 if (this.started
=== false) {
647 if (this.starting
=== false) {
648 this.starting
= true;
649 if (this.getEnableStatistics() === true) {
650 this.performanceStatistics
?.start();
652 if (this.hasFeatureProfile(SupportedFeatureProfiles
.Reservation
)) {
653 this.startReservationExpirationSetInterval();
655 this.openWSConnection();
656 // Monitor charging station template file
657 this.templateFileWatcher
= watchJsonFile(
659 FileType
.ChargingStationTemplate
,
662 (event
, filename
): void => {
663 if (Utils
.isNotEmptyString(filename
) && event
=== 'change') {
666 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
668 } file have changed, reload`
670 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
671 // FIXME: cleanup idtags cache if idtags file has changed
675 this.stopAutomaticTransactionGenerator();
676 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
677 this.startAutomaticTransactionGenerator();
679 if (this.getEnableStatistics() === true) {
680 this.performanceStatistics
?.restart();
682 this.performanceStatistics
?.stop();
684 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
687 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
695 parentPort
?.postMessage(buildStartedMessage(this));
696 this.starting
= false;
698 logger
.warn(`${this.logPrefix()} Charging station is already starting...`);
701 logger
.warn(`${this.logPrefix()} Charging station is already started...`);
705 public async stop(reason
?: StopTransactionReason
): Promise
<void> {
706 if (this.started
=== true) {
707 if (this.stopping
=== false) {
708 this.stopping
= true;
709 await this.stopMessageSequence(reason
);
710 this.closeWSConnection();
711 if (this.getEnableStatistics() === true) {
712 this.performanceStatistics
?.stop();
714 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
715 this.templateFileWatcher
?.close();
716 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
717 delete this.bootNotificationResponse
;
718 this.started
= false;
719 this.saveConfiguration();
720 parentPort
?.postMessage(buildStoppedMessage(this));
721 this.stopping
= false;
723 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`);
726 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`);
730 public async reset(reason
?: StopTransactionReason
): Promise
<void> {
731 await this.stop(reason
);
732 await Utils
.sleep(this.stationInfo
.resetTime
);
737 public saveOcppConfiguration(): void {
738 if (this.getOcppPersistentConfiguration()) {
739 this.saveConfiguration();
743 public hasFeatureProfile(featureProfile
: SupportedFeatureProfiles
): boolean | undefined {
744 return ChargingStationConfigurationUtils
.getConfigurationKey(
746 StandardParametersKey
.SupportedFeatureProfiles
747 )?.value
?.includes(featureProfile
);
750 public bufferMessage(message
: string): void {
751 this.messageBuffer
.add(message
);
754 public openWSConnection(
755 options
: WsOptions
= this.stationInfo
?.wsOptions
?? {},
756 params
: { closeOpened
?: boolean; terminateOpened
?: boolean } = {
758 terminateOpened
: false,
761 options
= { handshakeTimeout
: this.getConnectionTimeout() * 1000, ...options
};
762 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
};
763 if (this.started
=== false && this.starting
=== false) {
765 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()}
766 on stopped charging station`
771 !Utils
.isNullOrUndefined(this.stationInfo
.supervisionUser
) &&
772 !Utils
.isNullOrUndefined(this.stationInfo
.supervisionPassword
)
774 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
776 if (params
?.closeOpened
) {
777 this.closeWSConnection();
779 if (params
?.terminateOpened
) {
780 this.terminateWSConnection();
783 if (this.isWebSocketConnectionOpened() === true) {
785 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
792 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
795 this.wsConnection
= new WebSocket(
796 this.wsConnectionUrl
,
797 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
801 // Handle WebSocket message
802 this.wsConnection
.on(
804 this.onMessage
.bind(this) as (this: WebSocket
, data
: RawData
, isBinary
: boolean) => void
806 // Handle WebSocket error
807 this.wsConnection
.on(
809 this.onError
.bind(this) as (this: WebSocket
, error
: Error) => void
811 // Handle WebSocket close
812 this.wsConnection
.on(
814 this.onClose
.bind(this) as (this: WebSocket
, code
: number, reason
: Buffer
) => void
816 // Handle WebSocket open
817 this.wsConnection
.on('open', this.onOpen
.bind(this) as (this: WebSocket
) => void);
818 // Handle WebSocket ping
819 this.wsConnection
.on('ping', this.onPing
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
820 // Handle WebSocket pong
821 this.wsConnection
.on('pong', this.onPong
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
824 public closeWSConnection(): void {
825 if (this.isWebSocketConnectionOpened() === true) {
826 this.wsConnection
?.close();
827 this.wsConnection
= null;
831 public getAutomaticTransactionGeneratorConfiguration():
832 | AutomaticTransactionGeneratorConfiguration
834 let automaticTransactionGeneratorConfiguration
:
835 | AutomaticTransactionGeneratorConfiguration
837 const automaticTransactionGeneratorConfigurationFromFile
=
838 this.getConfigurationFromFile()?.automaticTransactionGenerator
;
840 this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
841 automaticTransactionGeneratorConfigurationFromFile
843 automaticTransactionGeneratorConfiguration
=
844 automaticTransactionGeneratorConfigurationFromFile
;
846 automaticTransactionGeneratorConfiguration
=
847 this.getTemplateFromFile()?.AutomaticTransactionGenerator
;
850 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
851 ...automaticTransactionGeneratorConfiguration
,
855 public getAutomaticTransactionGeneratorStatuses(): Status
[] | undefined {
856 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
;
859 public startAutomaticTransactionGenerator(connectorIds
?: number[]): void {
860 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this);
861 if (Utils
.isNotEmptyArray(connectorIds
)) {
862 for (const connectorId
of connectorIds
) {
863 this.automaticTransactionGenerator
?.startConnector(connectorId
);
866 this.automaticTransactionGenerator
?.start();
868 this.saveAutomaticTransactionGeneratorConfiguration();
869 parentPort
?.postMessage(buildUpdatedMessage(this));
872 public stopAutomaticTransactionGenerator(connectorIds
?: number[]): void {
873 if (Utils
.isNotEmptyArray(connectorIds
)) {
874 for (const connectorId
of connectorIds
) {
875 this.automaticTransactionGenerator
?.stopConnector(connectorId
);
878 this.automaticTransactionGenerator
?.stop();
880 this.saveAutomaticTransactionGeneratorConfiguration();
881 parentPort
?.postMessage(buildUpdatedMessage(this));
884 public async stopTransactionOnConnector(
886 reason
= StopTransactionReason
.NONE
887 ): Promise
<StopTransactionResponse
> {
888 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
;
890 this.getBeginEndMeterValues() === true &&
891 this.getOcppStrictCompliance() === true &&
892 this.getOutOfOrderEndMeterValues() === false
894 // FIXME: Implement OCPP version agnostic helpers
895 const transactionEndMeterValue
= OCPP16ServiceUtils
.buildTransactionEndMeterValue(
898 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
900 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
902 RequestCommand
.METER_VALUES
,
906 meterValue
: [transactionEndMeterValue
],
910 return this.ocppRequestService
.requestHandler
<StopTransactionRequest
, StopTransactionResponse
>(
912 RequestCommand
.STOP_TRANSACTION
,
915 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
921 public getReservationOnConnectorId0Enabled(): boolean {
922 return Utils
.convertToBoolean(
923 ChargingStationConfigurationUtils
.getConfigurationKey(
925 StandardParametersKey
.ReserveConnectorZeroSupported
930 public async addReservation(reservation
: Reservation
): Promise
<void> {
931 const [exists
, reservationFound
] = this.doesReservationExists(reservation
);
933 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
);
935 this.getConnectorStatus(reservation
.connectorId
).reservation
= reservation
;
936 await OCPPServiceUtils
.sendAndSetConnectorStatus(
938 reservation
.connectorId
,
939 ConnectorStatusEnum
.Reserved
,
941 { send
: reservation
.connectorId
!== 0 }
945 public async removeReservation(
946 reservation
: Reservation
,
947 reason
?: ReservationTerminationReason
949 const connector
= this.getConnectorStatus(reservation
.connectorId
);
951 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
952 delete connector
.reservation
;
954 case ReservationTerminationReason
.TRANSACTION_STARTED
:
955 delete connector
.reservation
;
957 case ReservationTerminationReason
.RESERVATION_CANCELED
||
958 ReservationTerminationReason
.REPLACE_EXISTING
||
959 ReservationTerminationReason
.EXPIRED
:
960 await OCPPServiceUtils
.sendAndSetConnectorStatus(
962 reservation
.connectorId
,
963 ConnectorStatusEnum
.Available
,
965 { send
: reservation
.connectorId
!== 0 }
967 delete connector
.reservation
;
974 public getReservationBy(
975 filterKey
: ReservationFilterKey
,
976 value
: number | string
977 ): Reservation
| undefined {
979 for (const evseStatus
of this.evses
.values()) {
980 for (const connectorStatus
of evseStatus
.connectors
.values()) {
981 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
982 return connectorStatus
.reservation
;
987 for (const connectorStatus
of this.connectors
.values()) {
988 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
989 return connectorStatus
.reservation
;
995 public doesReservationExists(reservation
: Partial
<Reservation
>): [boolean, Reservation
] {
996 const foundReservation
= this.getReservationBy(
997 ReservationFilterKey
.RESERVATION_ID
,
1000 return Utils
.isUndefined(foundReservation
) ? [false, null] : [true, foundReservation
];
1003 public startReservationExpirationSetInterval(customInterval
?: number): void {
1005 customInterval
?? Constants
.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL
;
1007 `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
1008 and starts on charging station now`
1010 // eslint-disable-next-line @typescript-eslint/no-misused-promises
1011 this.reservationExpirationSetInterval
= setInterval(async (): Promise
<void> => {
1012 const now
= new Date();
1013 if (this.hasEvses
) {
1014 for (const evseStatus
of this.evses
.values()) {
1015 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1016 if (connectorStatus
?.reservation
?.expiryDate
< now
) {
1017 await this.removeReservation(
1018 connectorStatus
.reservation
,
1019 ReservationTerminationReason
.EXPIRED
1025 for (const connectorStatus
of this.connectors
.values()) {
1026 if (connectorStatus
?.reservation
?.expiryDate
< now
) {
1027 await this.removeReservation(
1028 connectorStatus
.reservation
,
1029 ReservationTerminationReason
.EXPIRED
1037 public restartReservationExpiryDateSetInterval(): void {
1038 this.stopReservationExpirationSetInterval();
1039 this.startReservationExpirationSetInterval();
1042 public validateIncomingRequestWithReservation(connectorId
: number, idTag
: string): boolean {
1043 return this.getReservationBy(ReservationFilterKey
.CONNECTOR_ID
, connectorId
)?.idTag
=== idTag
;
1046 public isConnectorReservable(
1047 reservationId
: number,
1049 connectorId
?: number
1051 const [alreadyExists
] = this.doesReservationExists({ id
: reservationId
});
1052 if (alreadyExists
) {
1053 return alreadyExists
;
1055 const userReservedAlready
= Utils
.isUndefined(
1056 this.getReservationBy(ReservationFilterKey
.ID_TAG
, idTag
)
1060 const notConnectorZero
= Utils
.isUndefined(connectorId
) ? true : connectorId
> 0;
1061 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0;
1062 return !alreadyExists
&& !userReservedAlready
&& notConnectorZero
&& freeConnectorsAvailable
;
1065 private getNumberOfReservableConnectors(): number {
1066 let reservableConnectors
= 0;
1067 if (this.hasEvses
) {
1068 for (const evseStatus
of this.evses
.values()) {
1069 reservableConnectors
+= ChargingStationUtils
.countReservableConnectors(
1070 evseStatus
.connectors
1074 reservableConnectors
= ChargingStationUtils
.countReservableConnectors(this.connectors
);
1076 return reservableConnectors
- this.getNumberOfReservationsOnConnectorZero();
1079 private getNumberOfReservationsOnConnectorZero(): number {
1080 let numberOfReservations
= 0;
1081 if (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
) {
1082 ++numberOfReservations
;
1083 } else if (this.connectors
.get(0)?.reservation
) {
1084 ++numberOfReservations
;
1086 return numberOfReservations
;
1089 private flushMessageBuffer(): void {
1090 if (this.messageBuffer
.size
> 0) {
1091 for (const message
of this.messageBuffer
.values()) {
1092 let beginId
: string;
1093 let commandName
: RequestCommand
;
1094 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
;
1095 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
;
1097 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
;
1098 beginId
= PerformanceStatistics
.beginMeasure(commandName
);
1100 this.wsConnection
?.send(message
);
1101 isRequest
&& PerformanceStatistics
.endMeasure(commandName
, beginId
);
1103 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1105 )} payload sent: ${message}`
1107 this.messageBuffer
.delete(message
);
1112 private getSupervisionUrlOcppConfiguration(): boolean {
1113 return this.stationInfo
.supervisionUrlOcppConfiguration
?? false;
1116 private stopReservationExpirationSetInterval(): void {
1117 if (this.reservationExpirationSetInterval
) {
1118 clearInterval(this.reservationExpirationSetInterval
);
1122 private getSupervisionUrlOcppKey(): string {
1123 return this.stationInfo
.supervisionUrlOcppKey
?? VendorParametersKey
.ConnectionUrl
;
1126 private getTemplateFromFile(): ChargingStationTemplate
| undefined {
1127 let template
: ChargingStationTemplate
;
1129 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1130 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
);
1132 const measureId
= `${FileType.ChargingStationTemplate} read`;
1133 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1134 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
;
1135 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1136 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1137 .update(JSON
.stringify(template
))
1139 this.sharedLRUCache
.setChargingStationTemplate(template
);
1140 this.templateFileHash
= template
.templateHash
;
1143 handleFileException(
1145 FileType
.ChargingStationTemplate
,
1146 error
as NodeJS
.ErrnoException
,
1153 private getStationInfoFromTemplate(): ChargingStationInfo
{
1154 const stationTemplate
: ChargingStationTemplate
| undefined = this.getTemplateFromFile();
1155 ChargingStationUtils
.checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1156 ChargingStationUtils
.warnTemplateKeysDeprecation(
1161 if (stationTemplate
?.Connectors
) {
1162 ChargingStationUtils
.checkConnectorsConfiguration(
1168 const stationInfo
: ChargingStationInfo
=
1169 ChargingStationUtils
.stationTemplateToStationInfo(stationTemplate
);
1170 stationInfo
.hashId
= ChargingStationUtils
.getHashId(this.index
, stationTemplate
);
1171 stationInfo
.chargingStationId
= ChargingStationUtils
.getChargingStationId(
1175 stationInfo
.ocppVersion
= stationTemplate
?.ocppVersion
?? OCPPVersion
.VERSION_16
;
1176 ChargingStationUtils
.createSerialNumber(stationTemplate
, stationInfo
);
1177 if (Utils
.isNotEmptyArray(stationTemplate
?.power
)) {
1178 stationTemplate
.power
= stationTemplate
.power
as number[];
1179 const powerArrayRandomIndex
= Math.floor(Utils
.secureRandom() * stationTemplate
.power
.length
);
1180 stationInfo
.maximumPower
=
1181 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1182 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1183 : stationTemplate
.power
[powerArrayRandomIndex
];
1185 stationTemplate
.power
= stationTemplate
?.power
as number;
1186 stationInfo
.maximumPower
=
1187 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1188 ? stationTemplate
.power
* 1000
1189 : stationTemplate
.power
;
1191 stationInfo
.firmwareVersionPattern
=
1192 stationTemplate
?.firmwareVersionPattern
?? Constants
.SEMVER_PATTERN
;
1194 Utils
.isNotEmptyString(stationInfo
.firmwareVersion
) &&
1195 new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
) === false
1198 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1200 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1203 stationInfo
.firmwareUpgrade
= merge
<FirmwareUpgrade
>(
1210 stationTemplate
?.firmwareUpgrade
?? {}
1212 stationInfo
.resetTime
= !Utils
.isNullOrUndefined(stationTemplate
?.resetTime
)
1213 ? stationTemplate
.resetTime
* 1000
1214 : Constants
.CHARGING_STATION_DEFAULT_RESET_TIME
;
1215 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
);
1219 private getStationInfoFromFile(): ChargingStationInfo
| undefined {
1220 let stationInfo
: ChargingStationInfo
| undefined;
1221 if (this.getStationInfoPersistentConfiguration()) {
1222 stationInfo
= this.getConfigurationFromFile()?.stationInfo
;
1224 delete stationInfo
?.infoHash
;
1230 private getStationInfo(): ChargingStationInfo
{
1231 const stationInfoFromTemplate
: ChargingStationInfo
= this.getStationInfoFromTemplate();
1232 const stationInfoFromFile
: ChargingStationInfo
| undefined = this.getStationInfoFromFile();
1234 // 1. charging station info from template
1235 // 2. charging station info from configuration file
1236 if (stationInfoFromFile
?.templateHash
=== stationInfoFromTemplate
.templateHash
) {
1237 return stationInfoFromFile
;
1239 stationInfoFromFile
&&
1240 ChargingStationUtils
.propagateSerialNumber(
1241 this.getTemplateFromFile(),
1242 stationInfoFromFile
,
1243 stationInfoFromTemplate
1245 return stationInfoFromTemplate
;
1248 private saveStationInfo(): void {
1249 if (this.getStationInfoPersistentConfiguration()) {
1250 this.saveConfiguration();
1254 private getOcppPersistentConfiguration(): boolean {
1255 return this.stationInfo
?.ocppPersistentConfiguration
?? true;
1258 private getStationInfoPersistentConfiguration(): boolean {
1259 return this.stationInfo
?.stationInfoPersistentConfiguration
?? true;
1262 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1263 return this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
?? true;
1266 private handleUnsupportedVersion(version
: OCPPVersion
) {
1267 const errorMsg
= `Unsupported protocol version '${version}' configured
1268 in template file ${this.templateFile}`;
1269 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1270 throw new BaseError(errorMsg
);
1273 private initialize(): void {
1274 const stationTemplate
= this.getTemplateFromFile();
1275 ChargingStationUtils
.checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1276 this.configurationFile
= join(
1277 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1278 `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json`
1280 const chargingStationConfiguration
= this.getConfigurationFromFile();
1282 chargingStationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
1283 (chargingStationConfiguration
?.connectorsStatus
|| chargingStationConfiguration
?.evsesStatus
)
1285 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration
);
1287 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
);
1289 this.stationInfo
= this.getStationInfo();
1291 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1292 Utils
.isNotEmptyString(this.stationInfo
.firmwareVersion
) &&
1293 Utils
.isNotEmptyString(this.stationInfo
.firmwareVersionPattern
)
1295 const patternGroup
: number | undefined =
1296 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1297 this.stationInfo
.firmwareVersion
?.split('.').length
;
1298 const match
= this.stationInfo
?.firmwareVersion
1299 ?.match(new RegExp(this.stationInfo
.firmwareVersionPattern
))
1300 ?.slice(1, patternGroup
+ 1);
1301 const patchLevelIndex
= match
.length
- 1;
1302 match
[patchLevelIndex
] = (
1303 Utils
.convertToInt(match
[patchLevelIndex
]) +
1304 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.step
1306 this.stationInfo
.firmwareVersion
= match
?.join('.');
1308 this.saveStationInfo();
1309 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
1310 if (this.getEnableStatistics() === true) {
1311 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1312 this.stationInfo
.hashId
,
1313 this.stationInfo
.chargingStationId
,
1314 this.configuredSupervisionUrl
1317 this.bootNotificationRequest
= ChargingStationUtils
.createBootNotificationRequest(
1320 this.powerDivider
= this.getPowerDivider();
1321 // OCPP configuration
1322 this.ocppConfiguration
= this.getOcppConfiguration();
1323 this.initializeOcppConfiguration();
1324 this.initializeOcppServices();
1325 if (this.stationInfo
?.autoRegister
=== true) {
1326 this.bootNotificationResponse
= {
1327 currentTime
: new Date(),
1328 interval
: this.getHeartbeatInterval() / 1000,
1329 status: RegistrationStatusEnumType
.ACCEPTED
,
1334 private initializeOcppServices(): void {
1335 const ocppVersion
= this.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
1336 switch (ocppVersion
) {
1337 case OCPPVersion
.VERSION_16
:
1338 this.ocppIncomingRequestService
=
1339 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>();
1340 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1341 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1344 case OCPPVersion
.VERSION_20
:
1345 case OCPPVersion
.VERSION_201
:
1346 this.ocppIncomingRequestService
=
1347 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>();
1348 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1349 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1353 this.handleUnsupportedVersion(ocppVersion
);
1358 private initializeOcppConfiguration(): void {
1360 !ChargingStationConfigurationUtils
.getConfigurationKey(
1362 StandardParametersKey
.HeartbeatInterval
1365 ChargingStationConfigurationUtils
.addConfigurationKey(
1367 StandardParametersKey
.HeartbeatInterval
,
1372 !ChargingStationConfigurationUtils
.getConfigurationKey(
1374 StandardParametersKey
.HeartBeatInterval
1377 ChargingStationConfigurationUtils
.addConfigurationKey(
1379 StandardParametersKey
.HeartBeatInterval
,
1385 this.getSupervisionUrlOcppConfiguration() &&
1386 Utils
.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1387 !ChargingStationConfigurationUtils
.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1389 ChargingStationConfigurationUtils
.addConfigurationKey(
1391 this.getSupervisionUrlOcppKey(),
1392 this.configuredSupervisionUrl
.href
,
1396 !this.getSupervisionUrlOcppConfiguration() &&
1397 Utils
.isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1398 ChargingStationConfigurationUtils
.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1400 ChargingStationConfigurationUtils
.deleteConfigurationKey(
1402 this.getSupervisionUrlOcppKey(),
1407 Utils
.isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1408 !ChargingStationConfigurationUtils
.getConfigurationKey(
1410 this.stationInfo
.amperageLimitationOcppKey
1413 ChargingStationConfigurationUtils
.addConfigurationKey(
1415 this.stationInfo
.amperageLimitationOcppKey
,
1417 this.stationInfo
.maximumAmperage
*
1418 ChargingStationUtils
.getAmperageLimitationUnitDivider(this.stationInfo
)
1423 !ChargingStationConfigurationUtils
.getConfigurationKey(
1425 StandardParametersKey
.SupportedFeatureProfiles
1428 ChargingStationConfigurationUtils
.addConfigurationKey(
1430 StandardParametersKey
.SupportedFeatureProfiles
,
1431 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1434 ChargingStationConfigurationUtils
.addConfigurationKey(
1436 StandardParametersKey
.NumberOfConnectors
,
1437 this.getNumberOfConnectors().toString(),
1442 !ChargingStationConfigurationUtils
.getConfigurationKey(
1444 StandardParametersKey
.MeterValuesSampledData
1447 ChargingStationConfigurationUtils
.addConfigurationKey(
1449 StandardParametersKey
.MeterValuesSampledData
,
1450 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1454 !ChargingStationConfigurationUtils
.getConfigurationKey(
1456 StandardParametersKey
.ConnectorPhaseRotation
1459 const connectorsPhaseRotation
: string[] = [];
1460 if (this.hasEvses
) {
1461 for (const evseStatus
of this.evses
.values()) {
1462 for (const connectorId
of evseStatus
.connectors
.keys()) {
1463 connectorsPhaseRotation
.push(
1464 ChargingStationUtils
.getPhaseRotationValue(connectorId
, this.getNumberOfPhases())
1469 for (const connectorId
of this.connectors
.keys()) {
1470 connectorsPhaseRotation
.push(
1471 ChargingStationUtils
.getPhaseRotationValue(connectorId
, this.getNumberOfPhases())
1475 ChargingStationConfigurationUtils
.addConfigurationKey(
1477 StandardParametersKey
.ConnectorPhaseRotation
,
1478 connectorsPhaseRotation
.toString()
1482 !ChargingStationConfigurationUtils
.getConfigurationKey(
1484 StandardParametersKey
.AuthorizeRemoteTxRequests
1487 ChargingStationConfigurationUtils
.addConfigurationKey(
1489 StandardParametersKey
.AuthorizeRemoteTxRequests
,
1494 !ChargingStationConfigurationUtils
.getConfigurationKey(
1496 StandardParametersKey
.LocalAuthListEnabled
1498 ChargingStationConfigurationUtils
.getConfigurationKey(
1500 StandardParametersKey
.SupportedFeatureProfiles
1501 )?.value
?.includes(SupportedFeatureProfiles
.LocalAuthListManagement
)
1503 ChargingStationConfigurationUtils
.addConfigurationKey(
1505 StandardParametersKey
.LocalAuthListEnabled
,
1510 !ChargingStationConfigurationUtils
.getConfigurationKey(
1512 StandardParametersKey
.ConnectionTimeOut
1515 ChargingStationConfigurationUtils
.addConfigurationKey(
1517 StandardParametersKey
.ConnectionTimeOut
,
1518 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1521 this.saveOcppConfiguration();
1524 private initializeConnectorsOrEvsesFromFile(configuration
: ChargingStationConfiguration
): void {
1525 if (configuration
?.connectorsStatus
&& !configuration
?.evsesStatus
) {
1526 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1527 this.connectors
.set(connectorId
, Utils
.cloneObject
<ConnectorStatus
>(connectorStatus
));
1529 } else if (configuration
?.evsesStatus
&& !configuration
?.connectorsStatus
) {
1530 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1531 const evseStatus
= Utils
.cloneObject
<EvseStatusConfiguration
>(evseStatusConfiguration
);
1532 delete evseStatus
.connectorsStatus
;
1533 this.evses
.set(evseId
, {
1534 ...(evseStatus
as EvseStatus
),
1535 connectors
: new Map
<number, ConnectorStatus
>(
1536 evseStatusConfiguration
.connectorsStatus
.map((connectorStatus
, connectorId
) => [
1543 } else if (configuration
?.evsesStatus
&& configuration
?.connectorsStatus
) {
1544 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1545 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1546 throw new BaseError(errorMsg
);
1548 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`;
1549 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1550 throw new BaseError(errorMsg
);
1554 private initializeConnectorsOrEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
) {
1555 if (stationTemplate
?.Connectors
&& !stationTemplate
?.Evses
) {
1556 this.initializeConnectorsFromTemplate(stationTemplate
);
1557 } else if (stationTemplate
?.Evses
&& !stationTemplate
?.Connectors
) {
1558 this.initializeEvsesFromTemplate(stationTemplate
);
1559 } else if (stationTemplate
?.Evses
&& stationTemplate
?.Connectors
) {
1560 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1561 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1562 throw new BaseError(errorMsg
);
1564 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`;
1565 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1566 throw new BaseError(errorMsg
);
1570 private initializeConnectorsFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1571 if (!stationTemplate
?.Connectors
&& this.connectors
.size
=== 0) {
1572 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1573 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1574 throw new BaseError(errorMsg
);
1576 if (!stationTemplate
?.Connectors
[0]) {
1578 `${this.logPrefix()} Charging station information from template ${
1580 } with no connector id 0 configuration`
1583 if (stationTemplate
?.Connectors
) {
1584 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1585 ChargingStationUtils
.checkConnectorsConfiguration(
1590 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1592 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1595 const connectorsConfigChanged
=
1596 this.connectors
?.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
;
1597 if (this.connectors
?.size
=== 0 || connectorsConfigChanged
) {
1598 connectorsConfigChanged
&& this.connectors
.clear();
1599 this.connectorsConfigurationHash
= connectorsConfigHash
;
1600 if (templateMaxConnectors
> 0) {
1601 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1603 connectorId
=== 0 &&
1604 (!stationTemplate
?.Connectors
[connectorId
] ||
1605 this.getUseConnectorId0(stationTemplate
) === false)
1609 const templateConnectorId
=
1610 connectorId
> 0 && stationTemplate
?.randomConnectors
1611 ? Utils
.getRandomInteger(templateMaxAvailableConnectors
, 1)
1613 const connectorStatus
= stationTemplate
?.Connectors
[templateConnectorId
];
1614 ChargingStationUtils
.checkStationInfoConnectorStatus(
1615 templateConnectorId
,
1620 this.connectors
.set(connectorId
, Utils
.cloneObject
<ConnectorStatus
>(connectorStatus
));
1622 ChargingStationUtils
.initializeConnectorsMapStatus(this.connectors
, this.logPrefix());
1623 this.saveConnectorsStatus();
1626 `${this.logPrefix()} Charging station information from template ${
1628 } with no connectors configuration defined, cannot create connectors`
1634 `${this.logPrefix()} Charging station information from template ${
1636 } with no connectors configuration defined, using already defined connectors`
1641 private initializeEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1642 if (!stationTemplate
?.Evses
&& this.evses
.size
=== 0) {
1643 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1644 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1645 throw new BaseError(errorMsg
);
1647 if (!stationTemplate
?.Evses
[0]) {
1649 `${this.logPrefix()} Charging station information from template ${
1651 } with no evse id 0 configuration`
1654 if (!stationTemplate
?.Evses
[0]?.Connectors
[0]) {
1656 `${this.logPrefix()} Charging station information from template ${
1658 } with evse id 0 with no connector id 0 configuration`
1661 if (stationTemplate
?.Evses
) {
1662 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1663 .update(JSON
.stringify(stationTemplate
?.Evses
))
1665 const evsesConfigChanged
=
1666 this.evses
?.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
;
1667 if (this.evses
?.size
=== 0 || evsesConfigChanged
) {
1668 evsesConfigChanged
&& this.evses
.clear();
1669 this.evsesConfigurationHash
= evsesConfigHash
;
1670 const templateMaxEvses
= ChargingStationUtils
.getMaxNumberOfEvses(stationTemplate
?.Evses
);
1671 if (templateMaxEvses
> 0) {
1672 for (const evse
in stationTemplate
.Evses
) {
1673 const evseId
= Utils
.convertToInt(evse
);
1674 this.evses
.set(evseId
, {
1675 connectors
: ChargingStationUtils
.buildConnectorsMap(
1676 stationTemplate
?.Evses
[evse
]?.Connectors
,
1680 availability
: AvailabilityType
.Operative
,
1682 ChargingStationUtils
.initializeConnectorsMapStatus(
1683 this.evses
.get(evseId
)?.connectors
,
1687 this.saveEvsesStatus();
1690 `${this.logPrefix()} Charging station information from template ${
1692 } with no evses configuration defined, cannot create evses`
1698 `${this.logPrefix()} Charging station information from template ${
1700 } with no evses configuration defined, using already defined evses`
1705 private getConfigurationFromFile(): ChargingStationConfiguration
| undefined {
1706 let configuration
: ChargingStationConfiguration
| undefined;
1707 if (Utils
.isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1709 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1710 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1711 this.configurationFileHash
1714 const measureId
= `${FileType.ChargingStationConfiguration} read`;
1715 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1716 configuration
= JSON
.parse(
1717 readFileSync(this.configurationFile
, 'utf8')
1718 ) as ChargingStationConfiguration
;
1719 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1720 this.sharedLRUCache
.setChargingStationConfiguration(configuration
);
1721 this.configurationFileHash
= configuration
.configurationHash
;
1724 handleFileException(
1725 this.configurationFile
,
1726 FileType
.ChargingStationConfiguration
,
1727 error
as NodeJS
.ErrnoException
,
1732 return configuration
;
1735 private saveAutomaticTransactionGeneratorConfiguration(): void {
1736 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1737 this.saveConfiguration();
1741 private saveConnectorsStatus() {
1742 this.saveConfiguration();
1745 private saveEvsesStatus() {
1746 this.saveConfiguration();
1749 private saveConfiguration(): void {
1750 if (Utils
.isNotEmptyString(this.configurationFile
)) {
1752 if (!existsSync(dirname(this.configurationFile
))) {
1753 mkdirSync(dirname(this.configurationFile
), { recursive
: true });
1755 let configurationData
: ChargingStationConfiguration
=
1756 Utils
.cloneObject
<ChargingStationConfiguration
>(this.getConfigurationFromFile()) ?? {};
1757 if (this.getStationInfoPersistentConfiguration() && this.stationInfo
) {
1758 configurationData
.stationInfo
= this.stationInfo
;
1760 delete configurationData
.stationInfo
;
1762 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration
?.configurationKey
) {
1763 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
;
1765 delete configurationData
.configurationKey
;
1767 configurationData
= merge
<ChargingStationConfiguration
>(
1769 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1772 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1773 !this.getAutomaticTransactionGeneratorConfiguration()
1775 delete configurationData
.automaticTransactionGenerator
;
1777 if (this.connectors
.size
> 0) {
1778 configurationData
.connectorsStatus
= buildConnectorsStatus(this);
1780 delete configurationData
.connectorsStatus
;
1782 if (this.evses
.size
> 0) {
1783 configurationData
.evsesStatus
= buildEvsesStatus(this);
1785 delete configurationData
.evsesStatus
;
1787 delete configurationData
.configurationHash
;
1788 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1791 stationInfo
: configurationData
.stationInfo
,
1792 configurationKey
: configurationData
.configurationKey
,
1793 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1794 } as ChargingStationConfiguration
)
1797 if (this.configurationFileHash
!== configurationHash
) {
1798 AsyncLock
.acquire(AsyncLockType
.configuration
)
1800 configurationData
.configurationHash
= configurationHash
;
1801 const measureId
= `${FileType.ChargingStationConfiguration} write`;
1802 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1803 const fileDescriptor
= openSync(this.configurationFile
, 'w');
1804 writeFileSync(fileDescriptor
, JSON
.stringify(configurationData
, null, 2), 'utf8');
1805 closeSync(fileDescriptor
);
1806 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1807 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
1808 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
);
1809 this.configurationFileHash
= configurationHash
;
1812 handleFileException(
1813 this.configurationFile
,
1814 FileType
.ChargingStationConfiguration
,
1815 error
as NodeJS
.ErrnoException
,
1820 AsyncLock
.release(AsyncLockType
.configuration
).catch(Constants
.EMPTY_FUNCTION
);
1824 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1825 this.configurationFile
1830 handleFileException(
1831 this.configurationFile
,
1832 FileType
.ChargingStationConfiguration
,
1833 error
as NodeJS
.ErrnoException
,
1839 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1844 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration
| undefined {
1845 return this.getTemplateFromFile()?.Configuration
;
1848 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration
| undefined {
1849 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
;
1850 if (this.getOcppPersistentConfiguration() === true && configurationKey
) {
1851 return { configurationKey
};
1856 private getOcppConfiguration(): ChargingStationOcppConfiguration
| undefined {
1857 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1858 this.getOcppConfigurationFromFile();
1859 if (!ocppConfiguration
) {
1860 ocppConfiguration
= this.getOcppConfigurationFromTemplate();
1862 return ocppConfiguration
;
1865 private async onOpen(): Promise
<void> {
1866 if (this.isWebSocketConnectionOpened() === true) {
1868 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1870 if (this.isRegistered() === false) {
1871 // Send BootNotification
1872 let registrationRetryCount
= 0;
1874 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1875 BootNotificationRequest
,
1876 BootNotificationResponse
1877 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1878 skipBufferingOnError
: true,
1880 if (this.isRegistered() === false) {
1881 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount
;
1883 this?.bootNotificationResponse
?.interval
1884 ? this.bootNotificationResponse
.interval
* 1000
1885 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1889 this.isRegistered() === false &&
1890 (registrationRetryCount
<= this.getRegistrationMaxRetries() ||
1891 this.getRegistrationMaxRetries() === -1)
1894 if (this.isRegistered() === true) {
1895 if (this.inAcceptedState() === true) {
1896 await this.startMessageSequence();
1900 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1903 this.wsConnectionRestarted
= false;
1904 this.autoReconnectRetryCount
= 0;
1905 parentPort
?.postMessage(buildUpdatedMessage(this));
1908 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1913 private async onClose(code
: number, reason
: Buffer
): Promise
<void> {
1916 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1917 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1919 `${this.logPrefix()} WebSocket normally closed with status '${Utils.getWebSocketCloseEventStatusString(
1921 )}' and reason '${reason.toString()}'`
1923 this.autoReconnectRetryCount
= 0;
1928 `${this.logPrefix()} WebSocket abnormally closed with status '${Utils.getWebSocketCloseEventStatusString(
1930 )}' and reason '${reason.toString()}'`
1932 this.started
=== true && (await this.reconnect());
1935 parentPort
?.postMessage(buildUpdatedMessage(this));
1938 private getCachedRequest(messageType
: MessageType
, messageId
: string): CachedRequest
| undefined {
1939 const cachedRequest
= this.requests
.get(messageId
);
1940 if (Array.isArray(cachedRequest
) === true) {
1941 return cachedRequest
;
1943 throw new OCPPError(
1944 ErrorType
.PROTOCOL_ERROR
,
1945 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1947 )} is not an array`,
1949 cachedRequest
as JsonType
1953 private async handleIncomingMessage(request
: IncomingRequest
): Promise
<void> {
1954 const [messageType
, messageId
, commandName
, commandPayload
] = request
;
1955 if (this.getEnableStatistics() === true) {
1956 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
1959 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1963 // Process the message
1964 await this.ocppIncomingRequestService
.incomingRequestHandler(
1972 private handleResponseMessage(response
: Response
): void {
1973 const [messageType
, messageId
, commandPayload
] = response
;
1974 if (this.requests
.has(messageId
) === false) {
1976 throw new OCPPError(
1977 ErrorType
.INTERNAL_ERROR
,
1978 `Response for unknown message id ${messageId}`,
1984 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
1989 `${this.logPrefix()} << Command '${
1990 requestCommandName ?? Constants.UNKNOWN_COMMAND
1991 }' received response payload: ${JSON.stringify(response)}`
1993 responseCallback(commandPayload
, requestPayload
);
1996 private handleErrorMessage(errorResponse
: ErrorResponse
): void {
1997 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
;
1998 if (this.requests
.has(messageId
) === false) {
2000 throw new OCPPError(
2001 ErrorType
.INTERNAL_ERROR
,
2002 `Error response for unknown message id ${messageId}`,
2004 { errorType
, errorMessage
, errorDetails
}
2007 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
);
2009 `${this.logPrefix()} << Command '${
2010 requestCommandName ?? Constants.UNKNOWN_COMMAND
2011 }' received error response payload: ${JSON.stringify(errorResponse)}`
2013 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
));
2016 private async onMessage(data
: RawData
): Promise
<void> {
2017 let request
: IncomingRequest
| Response
| ErrorResponse
;
2018 let messageType
: number;
2019 let errorMsg
: string;
2021 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
;
2022 if (Array.isArray(request
) === true) {
2023 [messageType
] = request
;
2024 // Check the type of message
2025 switch (messageType
) {
2027 case MessageType
.CALL_MESSAGE
:
2028 await this.handleIncomingMessage(request
as IncomingRequest
);
2031 case MessageType
.CALL_RESULT_MESSAGE
:
2032 this.handleResponseMessage(request
as Response
);
2035 case MessageType
.CALL_ERROR_MESSAGE
:
2036 this.handleErrorMessage(request
as ErrorResponse
);
2040 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2041 errorMsg
= `Wrong message type ${messageType}`;
2042 logger
.error(`${this.logPrefix()} ${errorMsg}`);
2043 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
);
2045 parentPort
?.postMessage(buildUpdatedMessage(this));
2047 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, 'Incoming message is not an array', null, {
2052 let commandName
: IncomingRequestCommand
;
2053 let requestCommandName
: RequestCommand
| IncomingRequestCommand
;
2054 let errorCallback
: ErrorCallback
;
2055 const [, messageId
] = request
;
2056 switch (messageType
) {
2057 case MessageType
.CALL_MESSAGE
:
2058 [, , commandName
] = request
as IncomingRequest
;
2060 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
);
2062 case MessageType
.CALL_RESULT_MESSAGE
:
2063 case MessageType
.CALL_ERROR_MESSAGE
:
2064 if (this.requests
.has(messageId
) === true) {
2065 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
);
2066 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2067 errorCallback(error
as OCPPError
, false);
2069 // Remove the request from the cache in case of error at response handling
2070 this.requests
.delete(messageId
);
2074 if (error
instanceof OCPPError
=== false) {
2076 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2077 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2078 }' message '${data.toString()}' handling is not an OCPPError:`,
2083 `${this.logPrefix()} Incoming OCPP command '${
2084 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2085 }' message '${data.toString()}'${
2086 messageType !== MessageType.CALL_MESSAGE
2087 ? ` matching cached request
'${JSON.stringify(this.requests.get(messageId))}'`
2089 } processing error:`,
2095 private onPing(): void {
2096 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2099 private onPong(): void {
2100 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2103 private onError(error
: WSError
): void {
2104 this.closeWSConnection();
2105 logger
.error(`${this.logPrefix()} WebSocket error:`, error
);
2108 private getEnergyActiveImportRegister(connectorStatus
: ConnectorStatus
, rounded
= false): number {
2109 if (this.getMeteringPerTransaction() === true) {
2112 ? Math.round(connectorStatus
?.transactionEnergyActiveImportRegisterValue
)
2113 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2118 ? Math.round(connectorStatus
?.energyActiveImportRegisterValue
)
2119 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2123 private getUseConnectorId0(stationTemplate
?: ChargingStationTemplate
): boolean {
2124 return stationTemplate
?.useConnectorId0
?? true;
2127 private async stopRunningTransactions(reason
= StopTransactionReason
.NONE
): Promise
<void> {
2128 if (this.hasEvses
) {
2129 for (const [evseId
, evseStatus
] of this.evses
) {
2133 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2134 if (connectorStatus
.transactionStarted
=== true) {
2135 await this.stopTransactionOnConnector(connectorId
, reason
);
2140 for (const connectorId
of this.connectors
.keys()) {
2141 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2142 await this.stopTransactionOnConnector(connectorId
, reason
);
2149 private getConnectionTimeout(): number {
2151 ChargingStationConfigurationUtils
.getConfigurationKey(
2153 StandardParametersKey
.ConnectionTimeOut
2158 ChargingStationConfigurationUtils
.getConfigurationKey(
2160 StandardParametersKey
.ConnectionTimeOut
2162 ) ?? Constants
.DEFAULT_CONNECTION_TIMEOUT
2165 return Constants
.DEFAULT_CONNECTION_TIMEOUT
;
2168 // -1 for unlimited, 0 for disabling
2169 private getAutoReconnectMaxRetries(): number | undefined {
2171 this.stationInfo
.autoReconnectMaxRetries
?? Configuration
.getAutoReconnectMaxRetries() ?? -1
2176 private getRegistrationMaxRetries(): number | undefined {
2177 return this.stationInfo
.registrationMaxRetries
?? -1;
2180 private getPowerDivider(): number {
2181 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors();
2182 if (this.stationInfo
?.powerSharedByConnectors
) {
2183 powerDivider
= this.getNumberOfRunningTransactions();
2185 return powerDivider
;
2188 private getMaximumAmperage(stationInfo
: ChargingStationInfo
): number | undefined {
2189 const maximumPower
= this.getMaximumPower(stationInfo
);
2190 switch (this.getCurrentOutType(stationInfo
)) {
2191 case CurrentType
.AC
:
2192 return ACElectricUtils
.amperagePerPhaseFromPower(
2193 this.getNumberOfPhases(stationInfo
),
2194 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2195 this.getVoltageOut(stationInfo
)
2197 case CurrentType
.DC
:
2198 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
));
2202 private getAmperageLimitation(): number | undefined {
2204 Utils
.isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2205 ChargingStationConfigurationUtils
.getConfigurationKey(
2207 this.stationInfo
.amperageLimitationOcppKey
2212 ChargingStationConfigurationUtils
.getConfigurationKey(
2214 this.stationInfo
.amperageLimitationOcppKey
2216 ) / ChargingStationUtils
.getAmperageLimitationUnitDivider(this.stationInfo
)
2221 private async startMessageSequence(): Promise
<void> {
2222 if (this.stationInfo
?.autoRegister
=== true) {
2223 await this.ocppRequestService
.requestHandler
<
2224 BootNotificationRequest
,
2225 BootNotificationResponse
2226 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2227 skipBufferingOnError
: true,
2230 // Start WebSocket ping
2231 this.startWebSocketPing();
2233 this.startHeartbeat();
2234 // Initialize connectors status
2235 if (this.hasEvses
) {
2236 for (const [evseId
, evseStatus
] of this.evses
) {
2238 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2239 const connectorBootStatus
= ChargingStationUtils
.getBootConnectorStatus(
2244 await OCPPServiceUtils
.sendAndSetConnectorStatus(
2247 connectorBootStatus
,
2254 for (const connectorId
of this.connectors
.keys()) {
2255 if (connectorId
> 0) {
2256 const connectorBootStatus
= ChargingStationUtils
.getBootConnectorStatus(
2259 this.getConnectorStatus(connectorId
)
2261 await OCPPServiceUtils
.sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
);
2265 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2266 await this.ocppRequestService
.requestHandler
<
2267 FirmwareStatusNotificationRequest
,
2268 FirmwareStatusNotificationResponse
2269 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2270 status: FirmwareStatus
.Installed
,
2272 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
;
2276 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2277 this.startAutomaticTransactionGenerator();
2279 this.wsConnectionRestarted
=== true && this.flushMessageBuffer();
2282 private async stopMessageSequence(
2283 reason
: StopTransactionReason
= StopTransactionReason
.NONE
2285 // Stop WebSocket ping
2286 this.stopWebSocketPing();
2288 this.stopHeartbeat();
2289 // Stop ongoing transactions
2290 if (this.automaticTransactionGenerator
?.started
=== true) {
2291 this.stopAutomaticTransactionGenerator();
2293 await this.stopRunningTransactions(reason
);
2295 if (this.hasEvses
) {
2296 for (const [evseId
, evseStatus
] of this.evses
) {
2298 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2299 await this.ocppRequestService
.requestHandler
<
2300 StatusNotificationRequest
,
2301 StatusNotificationResponse
2304 RequestCommand
.STATUS_NOTIFICATION
,
2305 OCPPServiceUtils
.buildStatusNotificationRequest(
2308 ConnectorStatusEnum
.Unavailable
,
2312 delete connectorStatus
?.status;
2317 for (const connectorId
of this.connectors
.keys()) {
2318 if (connectorId
> 0) {
2319 await this.ocppRequestService
.requestHandler
<
2320 StatusNotificationRequest
,
2321 StatusNotificationResponse
2324 RequestCommand
.STATUS_NOTIFICATION
,
2325 OCPPServiceUtils
.buildStatusNotificationRequest(
2328 ConnectorStatusEnum
.Unavailable
2331 delete this.getConnectorStatus(connectorId
)?.status;
2337 private startWebSocketPing(): void {
2338 const webSocketPingInterval
: number = ChargingStationConfigurationUtils
.getConfigurationKey(
2340 StandardParametersKey
.WebSocketPingInterval
2342 ? Utils
.convertToInt(
2343 ChargingStationConfigurationUtils
.getConfigurationKey(
2345 StandardParametersKey
.WebSocketPingInterval
2349 if (webSocketPingInterval
> 0 && !this.webSocketPingSetInterval
) {
2350 this.webSocketPingSetInterval
= setInterval(() => {
2351 if (this.isWebSocketConnectionOpened() === true) {
2352 this.wsConnection
?.ping();
2354 }, webSocketPingInterval
* 1000);
2356 `${this.logPrefix()} WebSocket ping started every ${Utils.formatDurationSeconds(
2357 webSocketPingInterval
2360 } else if (this.webSocketPingSetInterval
) {
2362 `${this.logPrefix()} WebSocket ping already started every ${Utils.formatDurationSeconds(
2363 webSocketPingInterval
2368 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2373 private stopWebSocketPing(): void {
2374 if (this.webSocketPingSetInterval
) {
2375 clearInterval(this.webSocketPingSetInterval
);
2376 delete this.webSocketPingSetInterval
;
2380 private getConfiguredSupervisionUrl(): URL
{
2381 let configuredSupervisionUrl
: string;
2382 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls();
2383 if (Utils
.isNotEmptyArray(supervisionUrls
)) {
2384 let configuredSupervisionUrlIndex
: number;
2385 switch (Configuration
.getSupervisionUrlDistribution()) {
2386 case SupervisionUrlDistribution
.RANDOM
:
2387 configuredSupervisionUrlIndex
= Math.floor(Utils
.secureRandom() * supervisionUrls
.length
);
2389 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2390 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2392 Object.values(SupervisionUrlDistribution
).includes(
2393 Configuration
.getSupervisionUrlDistribution()
2396 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2397 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2400 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
;
2403 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
];
2405 configuredSupervisionUrl
= supervisionUrls
as string;
2407 if (Utils
.isNotEmptyString(configuredSupervisionUrl
)) {
2408 return new URL(configuredSupervisionUrl
);
2410 const errorMsg
= 'No supervision url(s) configured';
2411 logger
.error(`${this.logPrefix()} ${errorMsg}`);
2412 throw new BaseError(`${errorMsg}`);
2415 private stopHeartbeat(): void {
2416 if (this.heartbeatSetInterval
) {
2417 clearInterval(this.heartbeatSetInterval
);
2418 delete this.heartbeatSetInterval
;
2422 private terminateWSConnection(): void {
2423 if (this.isWebSocketConnectionOpened() === true) {
2424 this.wsConnection
?.terminate();
2425 this.wsConnection
= null;
2429 private getReconnectExponentialDelay(): boolean {
2430 return this.stationInfo
?.reconnectExponentialDelay
?? false;
2433 private async reconnect(): Promise
<void> {
2434 // Stop WebSocket ping
2435 this.stopWebSocketPing();
2437 this.stopHeartbeat();
2438 // Stop the ATG if needed
2439 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure
=== true) {
2440 this.stopAutomaticTransactionGenerator();
2443 this.autoReconnectRetryCount
< this.getAutoReconnectMaxRetries() ||
2444 this.getAutoReconnectMaxRetries() === -1
2446 ++this.autoReconnectRetryCount
;
2447 const reconnectDelay
= this.getReconnectExponentialDelay()
2448 ? Utils
.exponentialDelay(this.autoReconnectRetryCount
)
2449 : this.getConnectionTimeout() * 1000;
2450 const reconnectDelayWithdraw
= 1000;
2451 const reconnectTimeout
=
2452 reconnectDelay
&& reconnectDelay
- reconnectDelayWithdraw
> 0
2453 ? reconnectDelay
- reconnectDelayWithdraw
2456 `${this.logPrefix()} WebSocket connection retry in ${Utils.roundTo(
2459 )}ms, timeout ${reconnectTimeout}ms`
2461 await Utils
.sleep(reconnectDelay
);
2463 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2465 this.openWSConnection(
2467 ...(this.stationInfo
?.wsOptions
?? {}),
2468 handshakeTimeout
: reconnectTimeout
,
2470 { closeOpened
: true }
2472 this.wsConnectionRestarted
= true;
2473 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2475 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2476 this.autoReconnectRetryCount
2477 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`