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
,
103 buildChargingStationAutomaticTransactionGeneratorConfiguration
,
104 buildConnectorsStatus
,
113 formatDurationMilliSeconds
,
114 formatDurationSeconds
,
116 getWebSocketCloseEventStatusString
,
130 export class ChargingStation
{
131 public readonly index
: number;
132 public readonly templateFile
: string;
133 public stationInfo
!: ChargingStationInfo
;
134 public started
: boolean;
135 public starting
: boolean;
136 public idTagsCache
: IdTagsCache
;
137 public automaticTransactionGenerator
!: AutomaticTransactionGenerator
| undefined;
138 public ocppConfiguration
!: ChargingStationOcppConfiguration
| undefined;
139 public wsConnection
!: WebSocket
| null;
140 public readonly connectors
: Map
<number, ConnectorStatus
>;
141 public readonly evses
: Map
<number, EvseStatus
>;
142 public readonly requests
: Map
<string, CachedRequest
>;
143 public performanceStatistics
!: PerformanceStatistics
| undefined;
144 public heartbeatSetInterval
!: NodeJS
.Timeout
;
145 public ocppRequestService
!: OCPPRequestService
;
146 public bootNotificationRequest
!: BootNotificationRequest
;
147 public bootNotificationResponse
!: BootNotificationResponse
| undefined;
148 public powerDivider
!: number;
149 private stopping
: boolean;
150 private configurationFile
!: string;
151 private configurationFileHash
!: string;
152 private connectorsConfigurationHash
!: string;
153 private evsesConfigurationHash
!: string;
154 private ocppIncomingRequestService
!: OCPPIncomingRequestService
;
155 private readonly messageBuffer
: Set
<string>;
156 private configuredSupervisionUrl
!: URL
;
157 private wsConnectionRestarted
: boolean;
158 private autoReconnectRetryCount
: number;
159 private templateFileWatcher
!: FSWatcher
| undefined;
160 private templateFileHash
!: string;
161 private readonly sharedLRUCache
: SharedLRUCache
;
162 private webSocketPingSetInterval
!: NodeJS
.Timeout
;
163 private readonly chargingStationWorkerBroadcastChannel
: ChargingStationWorkerBroadcastChannel
;
164 private reservationExpirationSetInterval
?: NodeJS
.Timeout
;
166 constructor(index
: number, templateFile
: string) {
167 this.started
= false;
168 this.starting
= false;
169 this.stopping
= false;
170 this.wsConnectionRestarted
= false;
171 this.autoReconnectRetryCount
= 0;
173 this.templateFile
= templateFile
;
174 this.connectors
= new Map
<number, ConnectorStatus
>();
175 this.evses
= new Map
<number, EvseStatus
>();
176 this.requests
= new Map
<string, CachedRequest
>();
177 this.messageBuffer
= new Set
<string>();
178 this.sharedLRUCache
= SharedLRUCache
.getInstance();
179 this.idTagsCache
= IdTagsCache
.getInstance();
180 this.chargingStationWorkerBroadcastChannel
= new ChargingStationWorkerBroadcastChannel(this);
185 public get
hasEvses(): boolean {
186 return this.connectors
.size
=== 0 && this.evses
.size
> 0;
189 private get
wsConnectionUrl(): URL
{
192 this.getSupervisionUrlOcppConfiguration() &&
193 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
195 ChargingStationConfigurationUtils.getConfigurationKey(
197 this.getSupervisionUrlOcppKey()
200 ? ChargingStationConfigurationUtils.getConfigurationKey(
202 this.getSupervisionUrlOcppKey()
204 : this.configuredSupervisionUrl.href
205 }/${this.stationInfo.chargingStationId}`
209 public logPrefix
= (): string => {
212 (isNotEmptyString(this?.stationInfo?.chargingStationId)
213 ? this?.stationInfo?.chargingStationId
214 : ChargingStationUtils.getChargingStationId(this.index, this.getTemplateFromFile())) ??
215 'Error at building log prefix'
220 public hasIdTags(): boolean {
221 return isNotEmptyArray(
222 this.idTagsCache
.getIdTags(ChargingStationUtils
.getIdTagsFile(this.stationInfo
))
226 public getEnableStatistics(): boolean {
227 return this.stationInfo
.enableStatistics
?? false;
230 public getMustAuthorizeAtRemoteStart(): boolean {
231 return this.stationInfo
.mustAuthorizeAtRemoteStart
?? true;
234 public getPayloadSchemaValidation(): boolean {
235 return this.stationInfo
.payloadSchemaValidation
?? true;
238 public getNumberOfPhases(stationInfo
?: ChargingStationInfo
): number | undefined {
239 const localStationInfo
: ChargingStationInfo
= stationInfo
?? this.stationInfo
;
240 switch (this.getCurrentOutType(stationInfo
)) {
242 return !isUndefined(localStationInfo
.numberOfPhases
) ? localStationInfo
.numberOfPhases
: 3;
248 public isWebSocketConnectionOpened(): boolean {
249 return this?.wsConnection
?.readyState
=== WebSocket
.OPEN
;
252 public getRegistrationStatus(): RegistrationStatusEnumType
| undefined {
253 return this?.bootNotificationResponse
?.status;
256 public inUnknownState(): boolean {
257 return isNullOrUndefined(this?.bootNotificationResponse
?.status);
260 public inPendingState(): boolean {
261 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.PENDING
;
264 public inAcceptedState(): boolean {
265 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.ACCEPTED
;
268 public inRejectedState(): boolean {
269 return this?.bootNotificationResponse
?.status === RegistrationStatusEnumType
.REJECTED
;
272 public isRegistered(): boolean {
274 this.inUnknownState() === false &&
275 (this.inAcceptedState() === true || this.inPendingState() === true)
279 public isChargingStationAvailable(): boolean {
280 return this.getConnectorStatus(0)?.availability
=== AvailabilityType
.Operative
;
283 public hasConnector(connectorId
: number): boolean {
285 for (const evseStatus
of this.evses
.values()) {
286 if (evseStatus
.connectors
.has(connectorId
)) {
292 return this.connectors
.has(connectorId
);
295 public isConnectorAvailable(connectorId
: number): boolean {
298 this.getConnectorStatus(connectorId
)?.availability
=== AvailabilityType
.Operative
302 public getNumberOfConnectors(): number {
304 let numberOfConnectors
= 0;
305 for (const [evseId
, evseStatus
] of this.evses
) {
307 numberOfConnectors
+= evseStatus
.connectors
.size
;
310 return numberOfConnectors
;
312 return this.connectors
.has(0) ? this.connectors
.size
- 1 : this.connectors
.size
;
315 public getNumberOfEvses(): number {
316 return this.evses
.has(0) ? this.evses
.size
- 1 : this.evses
.size
;
319 public getConnectorStatus(connectorId
: number): ConnectorStatus
| undefined {
321 for (const evseStatus
of this.evses
.values()) {
322 if (evseStatus
.connectors
.has(connectorId
)) {
323 return evseStatus
.connectors
.get(connectorId
);
328 return this.connectors
.get(connectorId
);
331 public getCurrentOutType(stationInfo
?: ChargingStationInfo
): CurrentType
{
332 return (stationInfo
?? this.stationInfo
)?.currentOutType
?? CurrentType
.AC
;
335 public getOcppStrictCompliance(): boolean {
336 return this.stationInfo
?.ocppStrictCompliance
?? false;
339 public getVoltageOut(stationInfo
?: ChargingStationInfo
): number | undefined {
340 const defaultVoltageOut
= ChargingStationUtils
.getDefaultVoltageOut(
341 this.getCurrentOutType(stationInfo
),
345 return (stationInfo
?? this.stationInfo
).voltageOut
?? defaultVoltageOut
;
348 public getMaximumPower(stationInfo
?: ChargingStationInfo
): number {
349 const localStationInfo
= stationInfo
?? this.stationInfo
;
350 return (localStationInfo
['maxPower'] as number) ?? localStationInfo
.maximumPower
;
353 public getConnectorMaximumAvailablePower(connectorId
: number): number {
354 let connectorAmperageLimitationPowerLimit
: number;
356 !isNullOrUndefined(this.getAmperageLimitation()) &&
357 this.getAmperageLimitation() < this.stationInfo
?.maximumAmperage
359 connectorAmperageLimitationPowerLimit
=
360 (this.getCurrentOutType() === CurrentType
.AC
361 ? ACElectricUtils
.powerTotal(
362 this.getNumberOfPhases(),
363 this.getVoltageOut(),
364 this.getAmperageLimitation() *
365 (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors())
367 : DCElectricUtils
.power(this.getVoltageOut(), this.getAmperageLimitation())) /
370 const connectorMaximumPower
= this.getMaximumPower() / this.powerDivider
;
371 const connectorChargingProfilesPowerLimit
=
372 ChargingStationUtils
.getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId
);
374 isNaN(connectorMaximumPower
) ? Infinity : connectorMaximumPower
,
375 isNaN(connectorAmperageLimitationPowerLimit
)
377 : connectorAmperageLimitationPowerLimit
,
378 isNaN(connectorChargingProfilesPowerLimit
) ? Infinity : connectorChargingProfilesPowerLimit
382 public getTransactionIdTag(transactionId
: number): string | undefined {
384 for (const evseStatus
of this.evses
.values()) {
385 for (const connectorStatus
of evseStatus
.connectors
.values()) {
386 if (connectorStatus
.transactionId
=== transactionId
) {
387 return connectorStatus
.transactionIdTag
;
392 for (const connectorId
of this.connectors
.keys()) {
393 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
394 return this.getConnectorStatus(connectorId
)?.transactionIdTag
;
400 public getNumberOfRunningTransactions(): number {
403 for (const [evseId
, evseStatus
] of this.evses
) {
407 for (const connectorStatus
of evseStatus
.connectors
.values()) {
408 if (connectorStatus
.transactionStarted
=== true) {
414 for (const connectorId
of this.connectors
.keys()) {
415 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
423 public getOutOfOrderEndMeterValues(): boolean {
424 return this.stationInfo
?.outOfOrderEndMeterValues
?? false;
427 public getBeginEndMeterValues(): boolean {
428 return this.stationInfo
?.beginEndMeterValues
?? false;
431 public getMeteringPerTransaction(): boolean {
432 return this.stationInfo
?.meteringPerTransaction
?? true;
435 public getTransactionDataMeterValues(): boolean {
436 return this.stationInfo
?.transactionDataMeterValues
?? false;
439 public getMainVoltageMeterValues(): boolean {
440 return this.stationInfo
?.mainVoltageMeterValues
?? true;
443 public getPhaseLineToLineVoltageMeterValues(): boolean {
444 return this.stationInfo
?.phaseLineToLineVoltageMeterValues
?? false;
447 public getCustomValueLimitationMeterValues(): boolean {
448 return this.stationInfo
?.customValueLimitationMeterValues
?? true;
451 public getConnectorIdByTransactionId(transactionId
: number): number | undefined {
453 for (const evseStatus
of this.evses
.values()) {
454 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
455 if (connectorStatus
.transactionId
=== transactionId
) {
461 for (const connectorId
of this.connectors
.keys()) {
462 if (this.getConnectorStatus(connectorId
)?.transactionId
=== transactionId
) {
469 public getEnergyActiveImportRegisterByTransactionId(
470 transactionId
: number,
473 return this.getEnergyActiveImportRegister(
474 this.getConnectorStatus(this.getConnectorIdByTransactionId(transactionId
)),
479 public getEnergyActiveImportRegisterByConnectorId(connectorId
: number, rounded
= false): number {
480 return this.getEnergyActiveImportRegister(this.getConnectorStatus(connectorId
), rounded
);
483 public getAuthorizeRemoteTxRequests(): boolean {
484 const authorizeRemoteTxRequests
= ChargingStationConfigurationUtils
.getConfigurationKey(
486 StandardParametersKey
.AuthorizeRemoteTxRequests
488 return authorizeRemoteTxRequests
? convertToBoolean(authorizeRemoteTxRequests
.value
) : false;
491 public getLocalAuthListEnabled(): boolean {
492 const localAuthListEnabled
= ChargingStationConfigurationUtils
.getConfigurationKey(
494 StandardParametersKey
.LocalAuthListEnabled
496 return localAuthListEnabled
? convertToBoolean(localAuthListEnabled
.value
) : false;
499 public getHeartbeatInterval(): number {
500 const HeartbeatInterval
= ChargingStationConfigurationUtils
.getConfigurationKey(
502 StandardParametersKey
.HeartbeatInterval
504 if (HeartbeatInterval
) {
505 return convertToInt(HeartbeatInterval
.value
) * 1000;
507 const HeartBeatInterval
= ChargingStationConfigurationUtils
.getConfigurationKey(
509 StandardParametersKey
.HeartBeatInterval
511 if (HeartBeatInterval
) {
512 return convertToInt(HeartBeatInterval
.value
) * 1000;
514 this.stationInfo
?.autoRegister
=== false &&
516 `${this.logPrefix()} Heartbeat interval configuration key not set, using default value: ${
517 Constants.DEFAULT_HEARTBEAT_INTERVAL
520 return Constants
.DEFAULT_HEARTBEAT_INTERVAL
;
523 public setSupervisionUrl(url
: string): void {
525 this.getSupervisionUrlOcppConfiguration() &&
526 isNotEmptyString(this.getSupervisionUrlOcppKey())
528 ChargingStationConfigurationUtils
.setConfigurationKeyValue(
530 this.getSupervisionUrlOcppKey(),
534 this.stationInfo
.supervisionUrls
= url
;
535 this.saveStationInfo();
536 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
540 public startHeartbeat(): void {
541 if (this.getHeartbeatInterval() > 0 && !this.heartbeatSetInterval
) {
542 this.heartbeatSetInterval
= setInterval(() => {
543 this.ocppRequestService
544 .requestHandler
<HeartbeatRequest
, HeartbeatResponse
>(this, RequestCommand
.HEARTBEAT
)
547 `${this.logPrefix()} Error while sending '${RequestCommand.HEARTBEAT}':`,
551 }, this.getHeartbeatInterval());
553 `${this.logPrefix()} Heartbeat started every ${formatDurationMilliSeconds(
554 this.getHeartbeatInterval()
557 } else if (this.heartbeatSetInterval
) {
559 `${this.logPrefix()} Heartbeat already started every ${formatDurationMilliSeconds(
560 this.getHeartbeatInterval()
565 `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
566 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.toString()}`
592 if (!this.getConnectorStatus(connectorId
)) {
594 `${this.logPrefix()} Trying to start MeterValues on non existing connector id
595 ${connectorId.toString()}`
599 if (this.getConnectorStatus(connectorId
)?.transactionStarted
=== false) {
601 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
602 with no transaction started`
606 this.getConnectorStatus(connectorId
)?.transactionStarted
=== true &&
607 isNullOrUndefined(this.getConnectorStatus(connectorId
)?.transactionId
)
610 `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
611 with no transaction id`
616 this.getConnectorStatus(connectorId
).transactionSetInterval
= setInterval(() => {
617 // FIXME: Implement OCPP version agnostic helpers
618 const meterValue
: MeterValue
= OCPP16ServiceUtils
.buildMeterValue(
621 this.getConnectorStatus(connectorId
).transactionId
,
624 this.ocppRequestService
625 .requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
627 RequestCommand
.METER_VALUES
,
630 transactionId
: this.getConnectorStatus(connectorId
)?.transactionId
,
631 meterValue
: [meterValue
],
636 `${this.logPrefix()} Error while sending '${RequestCommand.METER_VALUES}':`,
643 `${this.logPrefix()} Charging station ${
644 StandardParametersKey.MeterValueSampleInterval
645 } configuration set to ${interval}, not sending MeterValues`
650 public stopMeterValues(connectorId
: number) {
651 if (this.getConnectorStatus(connectorId
)?.transactionSetInterval
) {
652 clearInterval(this.getConnectorStatus(connectorId
)?.transactionSetInterval
);
656 public start(): void {
657 if (this.started
=== false) {
658 if (this.starting
=== false) {
659 this.starting
= true;
660 if (this.getEnableStatistics() === true) {
661 this.performanceStatistics
?.start();
663 if (this.hasFeatureProfile(SupportedFeatureProfiles
.Reservation
)) {
664 this.startReservationExpirationSetInterval();
666 this.openWSConnection();
667 // Monitor charging station template file
668 this.templateFileWatcher
= watchJsonFile(
670 FileType
.ChargingStationTemplate
,
673 (event
, filename
): void => {
674 if (isNotEmptyString(filename
) && event
=== 'change') {
677 `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
679 } file have changed, reload`
681 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
682 // FIXME: cleanup idtags cache if idtags file has changed
686 this.stopAutomaticTransactionGenerator();
687 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
688 this.startAutomaticTransactionGenerator();
690 if (this.getEnableStatistics() === true) {
691 this.performanceStatistics
?.restart();
693 this.performanceStatistics
?.stop();
695 // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
698 `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
706 parentPort
?.postMessage(buildStartedMessage(this));
707 this.starting
= false;
709 logger
.warn(`${this.logPrefix()} Charging station is already starting...`);
712 logger
.warn(`${this.logPrefix()} Charging station is already started...`);
716 public async stop(reason
?: StopTransactionReason
): Promise
<void> {
717 if (this.started
=== true) {
718 if (this.stopping
=== false) {
719 this.stopping
= true;
720 await this.stopMessageSequence(reason
);
721 this.closeWSConnection();
722 if (this.getEnableStatistics() === true) {
723 this.performanceStatistics
?.stop();
725 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
726 this.templateFileWatcher
?.close();
727 this.sharedLRUCache
.deleteChargingStationTemplate(this.templateFileHash
);
728 delete this.bootNotificationResponse
;
729 this.started
= false;
730 this.saveConfiguration();
731 parentPort
?.postMessage(buildStoppedMessage(this));
732 this.stopping
= false;
734 logger
.warn(`${this.logPrefix()} Charging station is already stopping...`);
737 logger
.warn(`${this.logPrefix()} Charging station is already stopped...`);
741 public async reset(reason
?: StopTransactionReason
): Promise
<void> {
742 await this.stop(reason
);
743 await sleep(this.stationInfo
.resetTime
);
748 public saveOcppConfiguration(): void {
749 if (this.getOcppPersistentConfiguration()) {
750 this.saveConfiguration();
754 public hasFeatureProfile(featureProfile
: SupportedFeatureProfiles
): boolean | undefined {
755 return ChargingStationConfigurationUtils
.getConfigurationKey(
757 StandardParametersKey
.SupportedFeatureProfiles
758 )?.value
?.includes(featureProfile
);
761 public bufferMessage(message
: string): void {
762 this.messageBuffer
.add(message
);
765 public openWSConnection(
766 options
: WsOptions
= this.stationInfo
?.wsOptions
?? {},
767 params
: { closeOpened
?: boolean; terminateOpened
?: boolean } = {
769 terminateOpened
: false,
772 options
= { handshakeTimeout
: this.getConnectionTimeout() * 1000, ...options
};
773 params
= { ...{ closeOpened
: false, terminateOpened
: false }, ...params
};
774 if (this.started
=== false && this.starting
=== false) {
776 `${this.logPrefix()} Cannot open OCPP connection to URL ${this.wsConnectionUrl.toString()}
777 on stopped charging station`
782 !isNullOrUndefined(this.stationInfo
.supervisionUser
) &&
783 !isNullOrUndefined(this.stationInfo
.supervisionPassword
)
785 options
.auth
= `${this.stationInfo.supervisionUser}:${this.stationInfo.supervisionPassword}`;
787 if (params
?.closeOpened
) {
788 this.closeWSConnection();
790 if (params
?.terminateOpened
) {
791 this.terminateWSConnection();
794 if (this.isWebSocketConnectionOpened() === true) {
796 `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
803 `${this.logPrefix()} Open OCPP connection to URL ${this.wsConnectionUrl.toString()}`
806 this.wsConnection
= new WebSocket(
807 this.wsConnectionUrl
,
808 `ocpp${this.stationInfo.ocppVersion ?? OCPPVersion.VERSION_16}`,
812 // Handle WebSocket message
813 this.wsConnection
.on(
815 this.onMessage
.bind(this) as (this: WebSocket
, data
: RawData
, isBinary
: boolean) => void
817 // Handle WebSocket error
818 this.wsConnection
.on(
820 this.onError
.bind(this) as (this: WebSocket
, error
: Error) => void
822 // Handle WebSocket close
823 this.wsConnection
.on(
825 this.onClose
.bind(this) as (this: WebSocket
, code
: number, reason
: Buffer
) => void
827 // Handle WebSocket open
828 this.wsConnection
.on('open', this.onOpen
.bind(this) as (this: WebSocket
) => void);
829 // Handle WebSocket ping
830 this.wsConnection
.on('ping', this.onPing
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
831 // Handle WebSocket pong
832 this.wsConnection
.on('pong', this.onPong
.bind(this) as (this: WebSocket
, data
: Buffer
) => void);
835 public closeWSConnection(): void {
836 if (this.isWebSocketConnectionOpened() === true) {
837 this.wsConnection
?.close();
838 this.wsConnection
= null;
842 public getAutomaticTransactionGeneratorConfiguration():
843 | AutomaticTransactionGeneratorConfiguration
845 let automaticTransactionGeneratorConfiguration
:
846 | AutomaticTransactionGeneratorConfiguration
848 const automaticTransactionGeneratorConfigurationFromFile
=
849 this.getConfigurationFromFile()?.automaticTransactionGenerator
;
851 this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
852 automaticTransactionGeneratorConfigurationFromFile
854 automaticTransactionGeneratorConfiguration
=
855 automaticTransactionGeneratorConfigurationFromFile
;
857 automaticTransactionGeneratorConfiguration
=
858 this.getTemplateFromFile()?.AutomaticTransactionGenerator
;
861 ...Constants
.DEFAULT_ATG_CONFIGURATION
,
862 ...automaticTransactionGeneratorConfiguration
,
866 public getAutomaticTransactionGeneratorStatuses(): Status
[] | undefined {
867 return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
;
870 public startAutomaticTransactionGenerator(connectorIds
?: number[]): void {
871 this.automaticTransactionGenerator
= AutomaticTransactionGenerator
.getInstance(this);
872 if (isNotEmptyArray(connectorIds
)) {
873 for (const connectorId
of connectorIds
) {
874 this.automaticTransactionGenerator
?.startConnector(connectorId
);
877 this.automaticTransactionGenerator
?.start();
879 this.saveAutomaticTransactionGeneratorConfiguration();
880 parentPort
?.postMessage(buildUpdatedMessage(this));
883 public stopAutomaticTransactionGenerator(connectorIds
?: number[]): void {
884 if (isNotEmptyArray(connectorIds
)) {
885 for (const connectorId
of connectorIds
) {
886 this.automaticTransactionGenerator
?.stopConnector(connectorId
);
889 this.automaticTransactionGenerator
?.stop();
891 this.saveAutomaticTransactionGeneratorConfiguration();
892 parentPort
?.postMessage(buildUpdatedMessage(this));
895 public async stopTransactionOnConnector(
897 reason
= StopTransactionReason
.NONE
898 ): Promise
<StopTransactionResponse
> {
899 const transactionId
= this.getConnectorStatus(connectorId
)?.transactionId
;
901 this.getBeginEndMeterValues() === true &&
902 this.getOcppStrictCompliance() === true &&
903 this.getOutOfOrderEndMeterValues() === false
905 // FIXME: Implement OCPP version agnostic helpers
906 const transactionEndMeterValue
= OCPP16ServiceUtils
.buildTransactionEndMeterValue(
909 this.getEnergyActiveImportRegisterByTransactionId(transactionId
)
911 await this.ocppRequestService
.requestHandler
<MeterValuesRequest
, MeterValuesResponse
>(
913 RequestCommand
.METER_VALUES
,
917 meterValue
: [transactionEndMeterValue
],
921 return this.ocppRequestService
.requestHandler
<StopTransactionRequest
, StopTransactionResponse
>(
923 RequestCommand
.STOP_TRANSACTION
,
926 meterStop
: this.getEnergyActiveImportRegisterByTransactionId(transactionId
, true),
932 public getReservationOnConnectorId0Enabled(): boolean {
933 return convertToBoolean(
934 ChargingStationConfigurationUtils
.getConfigurationKey(
936 StandardParametersKey
.ReserveConnectorZeroSupported
941 public async addReservation(reservation
: Reservation
): Promise
<void> {
942 const [exists
, reservationFound
] = this.doesReservationExists(reservation
);
944 await this.removeReservation(reservationFound
, ReservationTerminationReason
.REPLACE_EXISTING
);
946 this.getConnectorStatus(reservation
.connectorId
).reservation
= reservation
;
947 await OCPPServiceUtils
.sendAndSetConnectorStatus(
949 reservation
.connectorId
,
950 ConnectorStatusEnum
.Reserved
,
952 { send
: reservation
.connectorId
!== 0 }
956 public async removeReservation(
957 reservation
: Reservation
,
958 reason
?: ReservationTerminationReason
960 const connector
= this.getConnectorStatus(reservation
.connectorId
);
962 case ReservationTerminationReason
.CONNECTOR_STATE_CHANGED
:
963 delete connector
.reservation
;
965 case ReservationTerminationReason
.TRANSACTION_STARTED
:
966 delete connector
.reservation
;
968 case ReservationTerminationReason
.RESERVATION_CANCELED
||
969 ReservationTerminationReason
.REPLACE_EXISTING
||
970 ReservationTerminationReason
.EXPIRED
:
971 await OCPPServiceUtils
.sendAndSetConnectorStatus(
973 reservation
.connectorId
,
974 ConnectorStatusEnum
.Available
,
976 { send
: reservation
.connectorId
!== 0 }
978 delete connector
.reservation
;
985 public getReservationBy(
986 filterKey
: ReservationFilterKey
,
987 value
: number | string
988 ): Reservation
| undefined {
990 for (const evseStatus
of this.evses
.values()) {
991 for (const connectorStatus
of evseStatus
.connectors
.values()) {
992 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
993 return connectorStatus
.reservation
;
998 for (const connectorStatus
of this.connectors
.values()) {
999 if (connectorStatus
?.reservation
?.[filterKey
] === value
) {
1000 return connectorStatus
.reservation
;
1006 public doesReservationExists(reservation
: Partial
<Reservation
>): [boolean, Reservation
] {
1007 const foundReservation
= this.getReservationBy(
1008 ReservationFilterKey
.RESERVATION_ID
,
1011 return isUndefined(foundReservation
) ? [false, null] : [true, foundReservation
];
1014 public startReservationExpirationSetInterval(customInterval
?: number): void {
1016 customInterval
?? Constants
.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL
;
1018 `${this.logPrefix()} Reservation expiration date interval is set to ${interval}
1019 and starts on charging station now`
1021 // eslint-disable-next-line @typescript-eslint/no-misused-promises
1022 this.reservationExpirationSetInterval
= setInterval(async (): Promise
<void> => {
1023 const now
= new Date();
1024 if (this.hasEvses
) {
1025 for (const evseStatus
of this.evses
.values()) {
1026 for (const connectorStatus
of evseStatus
.connectors
.values()) {
1027 if (connectorStatus
?.reservation
?.expiryDate
< now
) {
1028 await this.removeReservation(
1029 connectorStatus
.reservation
,
1030 ReservationTerminationReason
.EXPIRED
1036 for (const connectorStatus
of this.connectors
.values()) {
1037 if (connectorStatus
?.reservation
?.expiryDate
< now
) {
1038 await this.removeReservation(
1039 connectorStatus
.reservation
,
1040 ReservationTerminationReason
.EXPIRED
1048 public restartReservationExpiryDateSetInterval(): void {
1049 this.stopReservationExpirationSetInterval();
1050 this.startReservationExpirationSetInterval();
1053 public validateIncomingRequestWithReservation(connectorId
: number, idTag
: string): boolean {
1054 return this.getReservationBy(ReservationFilterKey
.CONNECTOR_ID
, connectorId
)?.idTag
=== idTag
;
1057 public isConnectorReservable(
1058 reservationId
: number,
1060 connectorId
?: number
1062 const [alreadyExists
] = this.doesReservationExists({ id
: reservationId
});
1063 if (alreadyExists
) {
1064 return alreadyExists
;
1066 const userReservedAlready
= isUndefined(
1067 this.getReservationBy(ReservationFilterKey
.ID_TAG
, idTag
)
1071 const notConnectorZero
= isUndefined(connectorId
) ? true : connectorId
> 0;
1072 const freeConnectorsAvailable
= this.getNumberOfReservableConnectors() > 0;
1073 return !alreadyExists
&& !userReservedAlready
&& notConnectorZero
&& freeConnectorsAvailable
;
1076 private getNumberOfReservableConnectors(): number {
1077 let reservableConnectors
= 0;
1078 if (this.hasEvses
) {
1079 for (const evseStatus
of this.evses
.values()) {
1080 reservableConnectors
+= ChargingStationUtils
.countReservableConnectors(
1081 evseStatus
.connectors
1085 reservableConnectors
= ChargingStationUtils
.countReservableConnectors(this.connectors
);
1087 return reservableConnectors
- this.getNumberOfReservationsOnConnectorZero();
1090 private getNumberOfReservationsOnConnectorZero(): number {
1091 let numberOfReservations
= 0;
1092 if (this.hasEvses
&& this.evses
.get(0)?.connectors
.get(0)?.reservation
) {
1093 ++numberOfReservations
;
1094 } else if (this.connectors
.get(0)?.reservation
) {
1095 ++numberOfReservations
;
1097 return numberOfReservations
;
1100 private flushMessageBuffer(): void {
1101 if (this.messageBuffer
.size
> 0) {
1102 for (const message
of this.messageBuffer
.values()) {
1103 let beginId
: string;
1104 let commandName
: RequestCommand
;
1105 const [messageType
] = JSON
.parse(message
) as OutgoingRequest
| Response
| ErrorResponse
;
1106 const isRequest
= messageType
=== MessageType
.CALL_MESSAGE
;
1108 [, , commandName
] = JSON
.parse(message
) as OutgoingRequest
;
1109 beginId
= PerformanceStatistics
.beginMeasure(commandName
);
1111 this.wsConnection
?.send(message
);
1112 isRequest
&& PerformanceStatistics
.endMeasure(commandName
, beginId
);
1114 `${this.logPrefix()} >> Buffered ${OCPPServiceUtils.getMessageTypeString(
1116 )} payload sent: ${message}`
1118 this.messageBuffer
.delete(message
);
1123 private getSupervisionUrlOcppConfiguration(): boolean {
1124 return this.stationInfo
.supervisionUrlOcppConfiguration
?? false;
1127 private stopReservationExpirationSetInterval(): void {
1128 if (this.reservationExpirationSetInterval
) {
1129 clearInterval(this.reservationExpirationSetInterval
);
1133 private getSupervisionUrlOcppKey(): string {
1134 return this.stationInfo
.supervisionUrlOcppKey
?? VendorParametersKey
.ConnectionUrl
;
1137 private getTemplateFromFile(): ChargingStationTemplate
| undefined {
1138 let template
: ChargingStationTemplate
;
1140 if (this.sharedLRUCache
.hasChargingStationTemplate(this.templateFileHash
)) {
1141 template
= this.sharedLRUCache
.getChargingStationTemplate(this.templateFileHash
);
1143 const measureId
= `${FileType.ChargingStationTemplate} read`;
1144 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1145 template
= JSON
.parse(readFileSync(this.templateFile
, 'utf8')) as ChargingStationTemplate
;
1146 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1147 template
.templateHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1148 .update(JSON
.stringify(template
))
1150 this.sharedLRUCache
.setChargingStationTemplate(template
);
1151 this.templateFileHash
= template
.templateHash
;
1154 handleFileException(
1156 FileType
.ChargingStationTemplate
,
1157 error
as NodeJS
.ErrnoException
,
1164 private getStationInfoFromTemplate(): ChargingStationInfo
{
1165 const stationTemplate
: ChargingStationTemplate
| undefined = this.getTemplateFromFile();
1166 ChargingStationUtils
.checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1167 ChargingStationUtils
.warnTemplateKeysDeprecation(
1172 if (stationTemplate
?.Connectors
) {
1173 ChargingStationUtils
.checkConnectorsConfiguration(
1179 const stationInfo
: ChargingStationInfo
=
1180 ChargingStationUtils
.stationTemplateToStationInfo(stationTemplate
);
1181 stationInfo
.hashId
= ChargingStationUtils
.getHashId(this.index
, stationTemplate
);
1182 stationInfo
.chargingStationId
= ChargingStationUtils
.getChargingStationId(
1186 stationInfo
.ocppVersion
= stationTemplate
?.ocppVersion
?? OCPPVersion
.VERSION_16
;
1187 ChargingStationUtils
.createSerialNumber(stationTemplate
, stationInfo
);
1188 if (isNotEmptyArray(stationTemplate
?.power
)) {
1189 stationTemplate
.power
= stationTemplate
.power
as number[];
1190 const powerArrayRandomIndex
= Math.floor(secureRandom() * stationTemplate
.power
.length
);
1191 stationInfo
.maximumPower
=
1192 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1193 ? stationTemplate
.power
[powerArrayRandomIndex
] * 1000
1194 : stationTemplate
.power
[powerArrayRandomIndex
];
1196 stationTemplate
.power
= stationTemplate
?.power
as number;
1197 stationInfo
.maximumPower
=
1198 stationTemplate
?.powerUnit
=== PowerUnits
.KILO_WATT
1199 ? stationTemplate
.power
* 1000
1200 : stationTemplate
.power
;
1202 stationInfo
.firmwareVersionPattern
=
1203 stationTemplate
?.firmwareVersionPattern
?? Constants
.SEMVER_PATTERN
;
1205 isNotEmptyString(stationInfo
.firmwareVersion
) &&
1206 new RegExp(stationInfo
.firmwareVersionPattern
).test(stationInfo
.firmwareVersion
) === false
1209 `${this.logPrefix()} Firmware version '${stationInfo.firmwareVersion}' in template file ${
1211 } does not match firmware version pattern '${stationInfo.firmwareVersionPattern}'`
1214 stationInfo
.firmwareUpgrade
= merge
<FirmwareUpgrade
>(
1221 stationTemplate
?.firmwareUpgrade
?? {}
1223 stationInfo
.resetTime
= !isNullOrUndefined(stationTemplate
?.resetTime
)
1224 ? stationTemplate
.resetTime
* 1000
1225 : Constants
.CHARGING_STATION_DEFAULT_RESET_TIME
;
1226 stationInfo
.maximumAmperage
= this.getMaximumAmperage(stationInfo
);
1230 private getStationInfoFromFile(): ChargingStationInfo
| undefined {
1231 let stationInfo
: ChargingStationInfo
| undefined;
1232 if (this.getStationInfoPersistentConfiguration()) {
1233 stationInfo
= this.getConfigurationFromFile()?.stationInfo
;
1235 delete stationInfo
?.infoHash
;
1241 private getStationInfo(): ChargingStationInfo
{
1242 const stationInfoFromTemplate
: ChargingStationInfo
= this.getStationInfoFromTemplate();
1243 const stationInfoFromFile
: ChargingStationInfo
| undefined = this.getStationInfoFromFile();
1245 // 1. charging station info from template
1246 // 2. charging station info from configuration file
1247 if (stationInfoFromFile
?.templateHash
=== stationInfoFromTemplate
.templateHash
) {
1248 return stationInfoFromFile
;
1250 stationInfoFromFile
&&
1251 ChargingStationUtils
.propagateSerialNumber(
1252 this.getTemplateFromFile(),
1253 stationInfoFromFile
,
1254 stationInfoFromTemplate
1256 return stationInfoFromTemplate
;
1259 private saveStationInfo(): void {
1260 if (this.getStationInfoPersistentConfiguration()) {
1261 this.saveConfiguration();
1265 private getOcppPersistentConfiguration(): boolean {
1266 return this.stationInfo
?.ocppPersistentConfiguration
?? true;
1269 private getStationInfoPersistentConfiguration(): boolean {
1270 return this.stationInfo
?.stationInfoPersistentConfiguration
?? true;
1273 private getAutomaticTransactionGeneratorPersistentConfiguration(): boolean {
1274 return this.stationInfo
?.automaticTransactionGeneratorPersistentConfiguration
?? true;
1277 private handleUnsupportedVersion(version
: OCPPVersion
) {
1278 const errorMsg
= `Unsupported protocol version '${version}' configured
1279 in template file ${this.templateFile}`;
1280 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1281 throw new BaseError(errorMsg
);
1284 private initialize(): void {
1285 const stationTemplate
= this.getTemplateFromFile();
1286 ChargingStationUtils
.checkTemplate(stationTemplate
, this.logPrefix(), this.templateFile
);
1287 this.configurationFile
= join(
1288 dirname(this.templateFile
.replace('station-templates', 'configurations')),
1289 `${ChargingStationUtils.getHashId(this.index, stationTemplate)}.json`
1291 const chargingStationConfiguration
= this.getConfigurationFromFile();
1293 chargingStationConfiguration
?.stationInfo
?.templateHash
=== stationTemplate
?.templateHash
&&
1294 (chargingStationConfiguration
?.connectorsStatus
|| chargingStationConfiguration
?.evsesStatus
)
1296 this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration
);
1298 this.initializeConnectorsOrEvsesFromTemplate(stationTemplate
);
1300 this.stationInfo
= this.getStationInfo();
1302 this.stationInfo
.firmwareStatus
=== FirmwareStatus
.Installing
&&
1303 isNotEmptyString(this.stationInfo
.firmwareVersion
) &&
1304 isNotEmptyString(this.stationInfo
.firmwareVersionPattern
)
1306 const patternGroup
: number | undefined =
1307 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.patternGroup
??
1308 this.stationInfo
.firmwareVersion
?.split('.').length
;
1309 const match
= this.stationInfo
?.firmwareVersion
1310 ?.match(new RegExp(this.stationInfo
.firmwareVersionPattern
))
1311 ?.slice(1, patternGroup
+ 1);
1312 const patchLevelIndex
= match
.length
- 1;
1313 match
[patchLevelIndex
] = (
1314 convertToInt(match
[patchLevelIndex
]) +
1315 this.stationInfo
.firmwareUpgrade
?.versionUpgrade
?.step
1317 this.stationInfo
.firmwareVersion
= match
?.join('.');
1319 this.saveStationInfo();
1320 this.configuredSupervisionUrl
= this.getConfiguredSupervisionUrl();
1321 if (this.getEnableStatistics() === true) {
1322 this.performanceStatistics
= PerformanceStatistics
.getInstance(
1323 this.stationInfo
.hashId
,
1324 this.stationInfo
.chargingStationId
,
1325 this.configuredSupervisionUrl
1328 this.bootNotificationRequest
= ChargingStationUtils
.createBootNotificationRequest(
1331 this.powerDivider
= this.getPowerDivider();
1332 // OCPP configuration
1333 this.ocppConfiguration
= this.getOcppConfiguration();
1334 this.initializeOcppConfiguration();
1335 this.initializeOcppServices();
1336 if (this.stationInfo
?.autoRegister
=== true) {
1337 this.bootNotificationResponse
= {
1338 currentTime
: new Date(),
1339 interval
: this.getHeartbeatInterval() / 1000,
1340 status: RegistrationStatusEnumType
.ACCEPTED
,
1345 private initializeOcppServices(): void {
1346 const ocppVersion
= this.stationInfo
.ocppVersion
?? OCPPVersion
.VERSION_16
;
1347 switch (ocppVersion
) {
1348 case OCPPVersion
.VERSION_16
:
1349 this.ocppIncomingRequestService
=
1350 OCPP16IncomingRequestService
.getInstance
<OCPP16IncomingRequestService
>();
1351 this.ocppRequestService
= OCPP16RequestService
.getInstance
<OCPP16RequestService
>(
1352 OCPP16ResponseService
.getInstance
<OCPP16ResponseService
>()
1355 case OCPPVersion
.VERSION_20
:
1356 case OCPPVersion
.VERSION_201
:
1357 this.ocppIncomingRequestService
=
1358 OCPP20IncomingRequestService
.getInstance
<OCPP20IncomingRequestService
>();
1359 this.ocppRequestService
= OCPP20RequestService
.getInstance
<OCPP20RequestService
>(
1360 OCPP20ResponseService
.getInstance
<OCPP20ResponseService
>()
1364 this.handleUnsupportedVersion(ocppVersion
);
1369 private initializeOcppConfiguration(): void {
1371 !ChargingStationConfigurationUtils
.getConfigurationKey(
1373 StandardParametersKey
.HeartbeatInterval
1376 ChargingStationConfigurationUtils
.addConfigurationKey(
1378 StandardParametersKey
.HeartbeatInterval
,
1383 !ChargingStationConfigurationUtils
.getConfigurationKey(
1385 StandardParametersKey
.HeartBeatInterval
1388 ChargingStationConfigurationUtils
.addConfigurationKey(
1390 StandardParametersKey
.HeartBeatInterval
,
1396 this.getSupervisionUrlOcppConfiguration() &&
1397 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1398 !ChargingStationConfigurationUtils
.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1400 ChargingStationConfigurationUtils
.addConfigurationKey(
1402 this.getSupervisionUrlOcppKey(),
1403 this.configuredSupervisionUrl
.href
,
1407 !this.getSupervisionUrlOcppConfiguration() &&
1408 isNotEmptyString(this.getSupervisionUrlOcppKey()) &&
1409 ChargingStationConfigurationUtils
.getConfigurationKey(this, this.getSupervisionUrlOcppKey())
1411 ChargingStationConfigurationUtils
.deleteConfigurationKey(
1413 this.getSupervisionUrlOcppKey(),
1418 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
1419 !ChargingStationConfigurationUtils
.getConfigurationKey(
1421 this.stationInfo
.amperageLimitationOcppKey
1424 ChargingStationConfigurationUtils
.addConfigurationKey(
1426 this.stationInfo
.amperageLimitationOcppKey
,
1428 this.stationInfo
.maximumAmperage
*
1429 ChargingStationUtils
.getAmperageLimitationUnitDivider(this.stationInfo
)
1434 !ChargingStationConfigurationUtils
.getConfigurationKey(
1436 StandardParametersKey
.SupportedFeatureProfiles
1439 ChargingStationConfigurationUtils
.addConfigurationKey(
1441 StandardParametersKey
.SupportedFeatureProfiles
,
1442 `${SupportedFeatureProfiles.Core},${SupportedFeatureProfiles.FirmwareManagement},${SupportedFeatureProfiles.LocalAuthListManagement},${SupportedFeatureProfiles.SmartCharging},${SupportedFeatureProfiles.RemoteTrigger}`
1445 ChargingStationConfigurationUtils
.addConfigurationKey(
1447 StandardParametersKey
.NumberOfConnectors
,
1448 this.getNumberOfConnectors().toString(),
1453 !ChargingStationConfigurationUtils
.getConfigurationKey(
1455 StandardParametersKey
.MeterValuesSampledData
1458 ChargingStationConfigurationUtils
.addConfigurationKey(
1460 StandardParametersKey
.MeterValuesSampledData
,
1461 MeterValueMeasurand
.ENERGY_ACTIVE_IMPORT_REGISTER
1465 !ChargingStationConfigurationUtils
.getConfigurationKey(
1467 StandardParametersKey
.ConnectorPhaseRotation
1470 const connectorsPhaseRotation
: string[] = [];
1471 if (this.hasEvses
) {
1472 for (const evseStatus
of this.evses
.values()) {
1473 for (const connectorId
of evseStatus
.connectors
.keys()) {
1474 connectorsPhaseRotation
.push(
1475 ChargingStationUtils
.getPhaseRotationValue(connectorId
, this.getNumberOfPhases())
1480 for (const connectorId
of this.connectors
.keys()) {
1481 connectorsPhaseRotation
.push(
1482 ChargingStationUtils
.getPhaseRotationValue(connectorId
, this.getNumberOfPhases())
1486 ChargingStationConfigurationUtils
.addConfigurationKey(
1488 StandardParametersKey
.ConnectorPhaseRotation
,
1489 connectorsPhaseRotation
.toString()
1493 !ChargingStationConfigurationUtils
.getConfigurationKey(
1495 StandardParametersKey
.AuthorizeRemoteTxRequests
1498 ChargingStationConfigurationUtils
.addConfigurationKey(
1500 StandardParametersKey
.AuthorizeRemoteTxRequests
,
1505 !ChargingStationConfigurationUtils
.getConfigurationKey(
1507 StandardParametersKey
.LocalAuthListEnabled
1509 ChargingStationConfigurationUtils
.getConfigurationKey(
1511 StandardParametersKey
.SupportedFeatureProfiles
1512 )?.value
?.includes(SupportedFeatureProfiles
.LocalAuthListManagement
)
1514 ChargingStationConfigurationUtils
.addConfigurationKey(
1516 StandardParametersKey
.LocalAuthListEnabled
,
1521 !ChargingStationConfigurationUtils
.getConfigurationKey(
1523 StandardParametersKey
.ConnectionTimeOut
1526 ChargingStationConfigurationUtils
.addConfigurationKey(
1528 StandardParametersKey
.ConnectionTimeOut
,
1529 Constants
.DEFAULT_CONNECTION_TIMEOUT
.toString()
1532 this.saveOcppConfiguration();
1535 private initializeConnectorsOrEvsesFromFile(configuration
: ChargingStationConfiguration
): void {
1536 if (configuration
?.connectorsStatus
&& !configuration
?.evsesStatus
) {
1537 for (const [connectorId
, connectorStatus
] of configuration
.connectorsStatus
.entries()) {
1538 this.connectors
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
1540 } else if (configuration
?.evsesStatus
&& !configuration
?.connectorsStatus
) {
1541 for (const [evseId
, evseStatusConfiguration
] of configuration
.evsesStatus
.entries()) {
1542 const evseStatus
= cloneObject
<EvseStatusConfiguration
>(evseStatusConfiguration
);
1543 delete evseStatus
.connectorsStatus
;
1544 this.evses
.set(evseId
, {
1545 ...(evseStatus
as EvseStatus
),
1546 connectors
: new Map
<number, ConnectorStatus
>(
1547 evseStatusConfiguration
.connectorsStatus
.map((connectorStatus
, connectorId
) => [
1554 } else if (configuration
?.evsesStatus
&& configuration
?.connectorsStatus
) {
1555 const errorMsg
= `Connectors and evses defined at the same time in configuration file ${this.configurationFile}`;
1556 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1557 throw new BaseError(errorMsg
);
1559 const errorMsg
= `No connectors or evses defined in configuration file ${this.configurationFile}`;
1560 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1561 throw new BaseError(errorMsg
);
1565 private initializeConnectorsOrEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
) {
1566 if (stationTemplate
?.Connectors
&& !stationTemplate
?.Evses
) {
1567 this.initializeConnectorsFromTemplate(stationTemplate
);
1568 } else if (stationTemplate
?.Evses
&& !stationTemplate
?.Connectors
) {
1569 this.initializeEvsesFromTemplate(stationTemplate
);
1570 } else if (stationTemplate
?.Evses
&& stationTemplate
?.Connectors
) {
1571 const errorMsg
= `Connectors and evses defined at the same time in template file ${this.templateFile}`;
1572 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1573 throw new BaseError(errorMsg
);
1575 const errorMsg
= `No connectors or evses defined in template file ${this.templateFile}`;
1576 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1577 throw new BaseError(errorMsg
);
1581 private initializeConnectorsFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1582 if (!stationTemplate
?.Connectors
&& this.connectors
.size
=== 0) {
1583 const errorMsg
= `No already defined connectors and charging station information from template ${this.templateFile} with no connectors configuration defined`;
1584 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1585 throw new BaseError(errorMsg
);
1587 if (!stationTemplate
?.Connectors
[0]) {
1589 `${this.logPrefix()} Charging station information from template ${
1591 } with no connector id 0 configuration`
1594 if (stationTemplate
?.Connectors
) {
1595 const { configuredMaxConnectors
, templateMaxConnectors
, templateMaxAvailableConnectors
} =
1596 ChargingStationUtils
.checkConnectorsConfiguration(
1601 const connectorsConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1603 `${JSON.stringify(stationTemplate?.Connectors)}${configuredMaxConnectors.toString()}`
1606 const connectorsConfigChanged
=
1607 this.connectors
?.size
!== 0 && this.connectorsConfigurationHash
!== connectorsConfigHash
;
1608 if (this.connectors
?.size
=== 0 || connectorsConfigChanged
) {
1609 connectorsConfigChanged
&& this.connectors
.clear();
1610 this.connectorsConfigurationHash
= connectorsConfigHash
;
1611 if (templateMaxConnectors
> 0) {
1612 for (let connectorId
= 0; connectorId
<= configuredMaxConnectors
; connectorId
++) {
1614 connectorId
=== 0 &&
1615 (!stationTemplate
?.Connectors
[connectorId
] ||
1616 this.getUseConnectorId0(stationTemplate
) === false)
1620 const templateConnectorId
=
1621 connectorId
> 0 && stationTemplate
?.randomConnectors
1622 ? getRandomInteger(templateMaxAvailableConnectors
, 1)
1624 const connectorStatus
= stationTemplate
?.Connectors
[templateConnectorId
];
1625 ChargingStationUtils
.checkStationInfoConnectorStatus(
1626 templateConnectorId
,
1631 this.connectors
.set(connectorId
, cloneObject
<ConnectorStatus
>(connectorStatus
));
1633 ChargingStationUtils
.initializeConnectorsMapStatus(this.connectors
, this.logPrefix());
1634 this.saveConnectorsStatus();
1637 `${this.logPrefix()} Charging station information from template ${
1639 } with no connectors configuration defined, cannot create connectors`
1645 `${this.logPrefix()} Charging station information from template ${
1647 } with no connectors configuration defined, using already defined connectors`
1652 private initializeEvsesFromTemplate(stationTemplate
: ChargingStationTemplate
): void {
1653 if (!stationTemplate
?.Evses
&& this.evses
.size
=== 0) {
1654 const errorMsg
= `No already defined evses and charging station information from template ${this.templateFile} with no evses configuration defined`;
1655 logger
.error(`${this.logPrefix()} ${errorMsg}`);
1656 throw new BaseError(errorMsg
);
1658 if (!stationTemplate
?.Evses
[0]) {
1660 `${this.logPrefix()} Charging station information from template ${
1662 } with no evse id 0 configuration`
1665 if (!stationTemplate
?.Evses
[0]?.Connectors
[0]) {
1667 `${this.logPrefix()} Charging station information from template ${
1669 } with evse id 0 with no connector id 0 configuration`
1672 if (stationTemplate
?.Evses
) {
1673 const evsesConfigHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1674 .update(JSON
.stringify(stationTemplate
?.Evses
))
1676 const evsesConfigChanged
=
1677 this.evses
?.size
!== 0 && this.evsesConfigurationHash
!== evsesConfigHash
;
1678 if (this.evses
?.size
=== 0 || evsesConfigChanged
) {
1679 evsesConfigChanged
&& this.evses
.clear();
1680 this.evsesConfigurationHash
= evsesConfigHash
;
1681 const templateMaxEvses
= ChargingStationUtils
.getMaxNumberOfEvses(stationTemplate
?.Evses
);
1682 if (templateMaxEvses
> 0) {
1683 for (const evse
in stationTemplate
.Evses
) {
1684 const evseId
= convertToInt(evse
);
1685 this.evses
.set(evseId
, {
1686 connectors
: ChargingStationUtils
.buildConnectorsMap(
1687 stationTemplate
?.Evses
[evse
]?.Connectors
,
1691 availability
: AvailabilityType
.Operative
,
1693 ChargingStationUtils
.initializeConnectorsMapStatus(
1694 this.evses
.get(evseId
)?.connectors
,
1698 this.saveEvsesStatus();
1701 `${this.logPrefix()} Charging station information from template ${
1703 } with no evses configuration defined, cannot create evses`
1709 `${this.logPrefix()} Charging station information from template ${
1711 } with no evses configuration defined, using already defined evses`
1716 private getConfigurationFromFile(): ChargingStationConfiguration
| undefined {
1717 let configuration
: ChargingStationConfiguration
| undefined;
1718 if (isNotEmptyString(this.configurationFile
) && existsSync(this.configurationFile
)) {
1720 if (this.sharedLRUCache
.hasChargingStationConfiguration(this.configurationFileHash
)) {
1721 configuration
= this.sharedLRUCache
.getChargingStationConfiguration(
1722 this.configurationFileHash
1725 const measureId
= `${FileType.ChargingStationConfiguration} read`;
1726 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1727 configuration
= JSON
.parse(
1728 readFileSync(this.configurationFile
, 'utf8')
1729 ) as ChargingStationConfiguration
;
1730 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1731 this.sharedLRUCache
.setChargingStationConfiguration(configuration
);
1732 this.configurationFileHash
= configuration
.configurationHash
;
1735 handleFileException(
1736 this.configurationFile
,
1737 FileType
.ChargingStationConfiguration
,
1738 error
as NodeJS
.ErrnoException
,
1743 return configuration
;
1746 private saveAutomaticTransactionGeneratorConfiguration(): void {
1747 if (this.getAutomaticTransactionGeneratorPersistentConfiguration()) {
1748 this.saveConfiguration();
1752 private saveConnectorsStatus() {
1753 this.saveConfiguration();
1756 private saveEvsesStatus() {
1757 this.saveConfiguration();
1760 private saveConfiguration(): void {
1761 if (isNotEmptyString(this.configurationFile
)) {
1763 if (!existsSync(dirname(this.configurationFile
))) {
1764 mkdirSync(dirname(this.configurationFile
), { recursive
: true });
1766 let configurationData
: ChargingStationConfiguration
=
1767 cloneObject
<ChargingStationConfiguration
>(this.getConfigurationFromFile()) ?? {};
1768 if (this.getStationInfoPersistentConfiguration() && this.stationInfo
) {
1769 configurationData
.stationInfo
= this.stationInfo
;
1771 delete configurationData
.stationInfo
;
1773 if (this.getOcppPersistentConfiguration() && this.ocppConfiguration
?.configurationKey
) {
1774 configurationData
.configurationKey
= this.ocppConfiguration
.configurationKey
;
1776 delete configurationData
.configurationKey
;
1778 configurationData
= merge
<ChargingStationConfiguration
>(
1780 buildChargingStationAutomaticTransactionGeneratorConfiguration(this)
1783 !this.getAutomaticTransactionGeneratorPersistentConfiguration() ||
1784 !this.getAutomaticTransactionGeneratorConfiguration()
1786 delete configurationData
.automaticTransactionGenerator
;
1788 if (this.connectors
.size
> 0) {
1789 configurationData
.connectorsStatus
= buildConnectorsStatus(this);
1791 delete configurationData
.connectorsStatus
;
1793 if (this.evses
.size
> 0) {
1794 configurationData
.evsesStatus
= buildEvsesStatus(this);
1796 delete configurationData
.evsesStatus
;
1798 delete configurationData
.configurationHash
;
1799 const configurationHash
= createHash(Constants
.DEFAULT_HASH_ALGORITHM
)
1802 stationInfo
: configurationData
.stationInfo
,
1803 configurationKey
: configurationData
.configurationKey
,
1804 automaticTransactionGenerator
: configurationData
.automaticTransactionGenerator
,
1805 } as ChargingStationConfiguration
)
1808 if (this.configurationFileHash
!== configurationHash
) {
1809 AsyncLock
.acquire(AsyncLockType
.configuration
)
1811 configurationData
.configurationHash
= configurationHash
;
1812 const measureId
= `${FileType.ChargingStationConfiguration} write`;
1813 const beginId
= PerformanceStatistics
.beginMeasure(measureId
);
1814 const fileDescriptor
= openSync(this.configurationFile
, 'w');
1815 writeFileSync(fileDescriptor
, JSON
.stringify(configurationData
, null, 2), 'utf8');
1816 closeSync(fileDescriptor
);
1817 PerformanceStatistics
.endMeasure(measureId
, beginId
);
1818 this.sharedLRUCache
.deleteChargingStationConfiguration(this.configurationFileHash
);
1819 this.sharedLRUCache
.setChargingStationConfiguration(configurationData
);
1820 this.configurationFileHash
= configurationHash
;
1823 handleFileException(
1824 this.configurationFile
,
1825 FileType
.ChargingStationConfiguration
,
1826 error
as NodeJS
.ErrnoException
,
1831 AsyncLock
.release(AsyncLockType
.configuration
).catch(Constants
.EMPTY_FUNCTION
);
1835 `${this.logPrefix()} Not saving unchanged charging station configuration file ${
1836 this.configurationFile
1841 handleFileException(
1842 this.configurationFile
,
1843 FileType
.ChargingStationConfiguration
,
1844 error
as NodeJS
.ErrnoException
,
1850 `${this.logPrefix()} Trying to save charging station configuration to undefined configuration file`
1855 private getOcppConfigurationFromTemplate(): ChargingStationOcppConfiguration
| undefined {
1856 return this.getTemplateFromFile()?.Configuration
;
1859 private getOcppConfigurationFromFile(): ChargingStationOcppConfiguration
| undefined {
1860 const configurationKey
= this.getConfigurationFromFile()?.configurationKey
;
1861 if (this.getOcppPersistentConfiguration() === true && configurationKey
) {
1862 return { configurationKey
};
1867 private getOcppConfiguration(): ChargingStationOcppConfiguration
| undefined {
1868 let ocppConfiguration
: ChargingStationOcppConfiguration
| undefined =
1869 this.getOcppConfigurationFromFile();
1870 if (!ocppConfiguration
) {
1871 ocppConfiguration
= this.getOcppConfigurationFromTemplate();
1873 return ocppConfiguration
;
1876 private async onOpen(): Promise
<void> {
1877 if (this.isWebSocketConnectionOpened() === true) {
1879 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} succeeded`
1881 if (this.isRegistered() === false) {
1882 // Send BootNotification
1883 let registrationRetryCount
= 0;
1885 this.bootNotificationResponse
= await this.ocppRequestService
.requestHandler
<
1886 BootNotificationRequest
,
1887 BootNotificationResponse
1888 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
1889 skipBufferingOnError
: true,
1891 if (this.isRegistered() === false) {
1892 this.getRegistrationMaxRetries() !== -1 && ++registrationRetryCount
;
1894 this?.bootNotificationResponse
?.interval
1895 ? this.bootNotificationResponse
.interval
* 1000
1896 : Constants
.DEFAULT_BOOT_NOTIFICATION_INTERVAL
1900 this.isRegistered() === false &&
1901 (registrationRetryCount
<= this.getRegistrationMaxRetries() ||
1902 this.getRegistrationMaxRetries() === -1)
1905 if (this.isRegistered() === true) {
1906 if (this.inAcceptedState() === true) {
1907 await this.startMessageSequence();
1911 `${this.logPrefix()} Registration failure: max retries reached (${this.getRegistrationMaxRetries()}) or retry disabled (${this.getRegistrationMaxRetries()})`
1914 this.wsConnectionRestarted
= false;
1915 this.autoReconnectRetryCount
= 0;
1916 parentPort
?.postMessage(buildUpdatedMessage(this));
1919 `${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`
1924 private async onClose(code
: number, reason
: Buffer
): Promise
<void> {
1927 case WebSocketCloseEventStatusCode
.CLOSE_NORMAL
:
1928 case WebSocketCloseEventStatusCode
.CLOSE_NO_STATUS
:
1930 `${this.logPrefix()} WebSocket normally closed with status '${getWebSocketCloseEventStatusString(
1932 )}' and reason '${reason.toString()}'`
1934 this.autoReconnectRetryCount
= 0;
1939 `${this.logPrefix()} WebSocket abnormally closed with status '${getWebSocketCloseEventStatusString(
1941 )}' and reason '${reason.toString()}'`
1943 this.started
=== true && (await this.reconnect());
1946 parentPort
?.postMessage(buildUpdatedMessage(this));
1949 private getCachedRequest(messageType
: MessageType
, messageId
: string): CachedRequest
| undefined {
1950 const cachedRequest
= this.requests
.get(messageId
);
1951 if (Array.isArray(cachedRequest
) === true) {
1952 return cachedRequest
;
1954 throw new OCPPError(
1955 ErrorType
.PROTOCOL_ERROR
,
1956 `Cached request for message id ${messageId} ${OCPPServiceUtils.getMessageTypeString(
1958 )} is not an array`,
1960 cachedRequest
as JsonType
1964 private async handleIncomingMessage(request
: IncomingRequest
): Promise
<void> {
1965 const [messageType
, messageId
, commandName
, commandPayload
] = request
;
1966 if (this.getEnableStatistics() === true) {
1967 this.performanceStatistics
?.addRequestStatistic(commandName
, messageType
);
1970 `${this.logPrefix()} << Command '${commandName}' received request payload: ${JSON.stringify(
1974 // Process the message
1975 await this.ocppIncomingRequestService
.incomingRequestHandler(
1983 private handleResponseMessage(response
: Response
): void {
1984 const [messageType
, messageId
, commandPayload
] = response
;
1985 if (this.requests
.has(messageId
) === false) {
1987 throw new OCPPError(
1988 ErrorType
.INTERNAL_ERROR
,
1989 `Response for unknown message id ${messageId}`,
1995 const [responseCallback
, , requestCommandName
, requestPayload
] = this.getCachedRequest(
2000 `${this.logPrefix()} << Command '${
2001 requestCommandName ?? Constants.UNKNOWN_COMMAND
2002 }' received response payload: ${JSON.stringify(response)}`
2004 responseCallback(commandPayload
, requestPayload
);
2007 private handleErrorMessage(errorResponse
: ErrorResponse
): void {
2008 const [messageType
, messageId
, errorType
, errorMessage
, errorDetails
] = errorResponse
;
2009 if (this.requests
.has(messageId
) === false) {
2011 throw new OCPPError(
2012 ErrorType
.INTERNAL_ERROR
,
2013 `Error response for unknown message id ${messageId}`,
2015 { errorType
, errorMessage
, errorDetails
}
2018 const [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
);
2020 `${this.logPrefix()} << Command '${
2021 requestCommandName ?? Constants.UNKNOWN_COMMAND
2022 }' received error response payload: ${JSON.stringify(errorResponse)}`
2024 errorCallback(new OCPPError(errorType
, errorMessage
, requestCommandName
, errorDetails
));
2027 private async onMessage(data
: RawData
): Promise
<void> {
2028 let request
: IncomingRequest
| Response
| ErrorResponse
;
2029 let messageType
: number;
2030 let errorMsg
: string;
2032 request
= JSON
.parse(data
.toString()) as IncomingRequest
| Response
| ErrorResponse
;
2033 if (Array.isArray(request
) === true) {
2034 [messageType
] = request
;
2035 // Check the type of message
2036 switch (messageType
) {
2038 case MessageType
.CALL_MESSAGE
:
2039 await this.handleIncomingMessage(request
as IncomingRequest
);
2042 case MessageType
.CALL_RESULT_MESSAGE
:
2043 this.handleResponseMessage(request
as Response
);
2046 case MessageType
.CALL_ERROR_MESSAGE
:
2047 this.handleErrorMessage(request
as ErrorResponse
);
2051 // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
2052 errorMsg
= `Wrong message type ${messageType}`;
2053 logger
.error(`${this.logPrefix()} ${errorMsg}`);
2054 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, errorMsg
);
2056 parentPort
?.postMessage(buildUpdatedMessage(this));
2058 throw new OCPPError(ErrorType
.PROTOCOL_ERROR
, 'Incoming message is not an array', null, {
2063 let commandName
: IncomingRequestCommand
;
2064 let requestCommandName
: RequestCommand
| IncomingRequestCommand
;
2065 let errorCallback
: ErrorCallback
;
2066 const [, messageId
] = request
;
2067 switch (messageType
) {
2068 case MessageType
.CALL_MESSAGE
:
2069 [, , commandName
] = request
as IncomingRequest
;
2071 await this.ocppRequestService
.sendError(this, messageId
, error
as OCPPError
, commandName
);
2073 case MessageType
.CALL_RESULT_MESSAGE
:
2074 case MessageType
.CALL_ERROR_MESSAGE
:
2075 if (this.requests
.has(messageId
) === true) {
2076 [, errorCallback
, requestCommandName
] = this.getCachedRequest(messageType
, messageId
);
2077 // Reject the deferred promise in case of error at response handling (rejecting an already fulfilled promise is a no-op)
2078 errorCallback(error
as OCPPError
, false);
2080 // Remove the request from the cache in case of error at response handling
2081 this.requests
.delete(messageId
);
2085 if (error
instanceof OCPPError
=== false) {
2087 `${this.logPrefix()} Error thrown at incoming OCPP command '${
2088 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2089 }' message '${data.toString()}' handling is not an OCPPError:`,
2094 `${this.logPrefix()} Incoming OCPP command '${
2095 commandName ?? requestCommandName ?? Constants.UNKNOWN_COMMAND
2096 }' message '${data.toString()}'${
2097 messageType !== MessageType.CALL_MESSAGE
2098 ? ` matching cached request
'${JSON.stringify(this.requests.get(messageId))}'`
2100 } processing error:`,
2106 private onPing(): void {
2107 logger
.debug(`${this.logPrefix()} Received a WS ping (rfc6455) from the server`);
2110 private onPong(): void {
2111 logger
.debug(`${this.logPrefix()} Received a WS pong (rfc6455) from the server`);
2114 private onError(error
: WSError
): void {
2115 this.closeWSConnection();
2116 logger
.error(`${this.logPrefix()} WebSocket error:`, error
);
2119 private getEnergyActiveImportRegister(connectorStatus
: ConnectorStatus
, rounded
= false): number {
2120 if (this.getMeteringPerTransaction() === true) {
2123 ? Math.round(connectorStatus
?.transactionEnergyActiveImportRegisterValue
)
2124 : connectorStatus
?.transactionEnergyActiveImportRegisterValue
) ?? 0
2129 ? Math.round(connectorStatus
?.energyActiveImportRegisterValue
)
2130 : connectorStatus
?.energyActiveImportRegisterValue
) ?? 0
2134 private getUseConnectorId0(stationTemplate
?: ChargingStationTemplate
): boolean {
2135 return stationTemplate
?.useConnectorId0
?? true;
2138 private async stopRunningTransactions(reason
= StopTransactionReason
.NONE
): Promise
<void> {
2139 if (this.hasEvses
) {
2140 for (const [evseId
, evseStatus
] of this.evses
) {
2144 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2145 if (connectorStatus
.transactionStarted
=== true) {
2146 await this.stopTransactionOnConnector(connectorId
, reason
);
2151 for (const connectorId
of this.connectors
.keys()) {
2152 if (connectorId
> 0 && this.getConnectorStatus(connectorId
)?.transactionStarted
=== true) {
2153 await this.stopTransactionOnConnector(connectorId
, reason
);
2160 private getConnectionTimeout(): number {
2162 ChargingStationConfigurationUtils
.getConfigurationKey(
2164 StandardParametersKey
.ConnectionTimeOut
2169 ChargingStationConfigurationUtils
.getConfigurationKey(
2171 StandardParametersKey
.ConnectionTimeOut
2173 ) ?? Constants
.DEFAULT_CONNECTION_TIMEOUT
2176 return Constants
.DEFAULT_CONNECTION_TIMEOUT
;
2179 // -1 for unlimited, 0 for disabling
2180 private getAutoReconnectMaxRetries(): number | undefined {
2182 this.stationInfo
.autoReconnectMaxRetries
?? Configuration
.getAutoReconnectMaxRetries() ?? -1
2187 private getRegistrationMaxRetries(): number | undefined {
2188 return this.stationInfo
.registrationMaxRetries
?? -1;
2191 private getPowerDivider(): number {
2192 let powerDivider
= this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors();
2193 if (this.stationInfo
?.powerSharedByConnectors
) {
2194 powerDivider
= this.getNumberOfRunningTransactions();
2196 return powerDivider
;
2199 private getMaximumAmperage(stationInfo
: ChargingStationInfo
): number | undefined {
2200 const maximumPower
= this.getMaximumPower(stationInfo
);
2201 switch (this.getCurrentOutType(stationInfo
)) {
2202 case CurrentType
.AC
:
2203 return ACElectricUtils
.amperagePerPhaseFromPower(
2204 this.getNumberOfPhases(stationInfo
),
2205 maximumPower
/ (this.hasEvses
? this.getNumberOfEvses() : this.getNumberOfConnectors()),
2206 this.getVoltageOut(stationInfo
)
2208 case CurrentType
.DC
:
2209 return DCElectricUtils
.amperage(maximumPower
, this.getVoltageOut(stationInfo
));
2213 private getAmperageLimitation(): number | undefined {
2215 isNotEmptyString(this.stationInfo
?.amperageLimitationOcppKey
) &&
2216 ChargingStationConfigurationUtils
.getConfigurationKey(
2218 this.stationInfo
.amperageLimitationOcppKey
2223 ChargingStationConfigurationUtils
.getConfigurationKey(
2225 this.stationInfo
.amperageLimitationOcppKey
2227 ) / ChargingStationUtils
.getAmperageLimitationUnitDivider(this.stationInfo
)
2232 private async startMessageSequence(): Promise
<void> {
2233 if (this.stationInfo
?.autoRegister
=== true) {
2234 await this.ocppRequestService
.requestHandler
<
2235 BootNotificationRequest
,
2236 BootNotificationResponse
2237 >(this, RequestCommand
.BOOT_NOTIFICATION
, this.bootNotificationRequest
, {
2238 skipBufferingOnError
: true,
2241 // Start WebSocket ping
2242 this.startWebSocketPing();
2244 this.startHeartbeat();
2245 // Initialize connectors status
2246 if (this.hasEvses
) {
2247 for (const [evseId
, evseStatus
] of this.evses
) {
2249 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2250 const connectorBootStatus
= ChargingStationUtils
.getBootConnectorStatus(
2255 await OCPPServiceUtils
.sendAndSetConnectorStatus(
2258 connectorBootStatus
,
2265 for (const connectorId
of this.connectors
.keys()) {
2266 if (connectorId
> 0) {
2267 const connectorBootStatus
= ChargingStationUtils
.getBootConnectorStatus(
2270 this.getConnectorStatus(connectorId
)
2272 await OCPPServiceUtils
.sendAndSetConnectorStatus(this, connectorId
, connectorBootStatus
);
2276 if (this.stationInfo
?.firmwareStatus
=== FirmwareStatus
.Installing
) {
2277 await this.ocppRequestService
.requestHandler
<
2278 FirmwareStatusNotificationRequest
,
2279 FirmwareStatusNotificationResponse
2280 >(this, RequestCommand
.FIRMWARE_STATUS_NOTIFICATION
, {
2281 status: FirmwareStatus
.Installed
,
2283 this.stationInfo
.firmwareStatus
= FirmwareStatus
.Installed
;
2287 if (this.getAutomaticTransactionGeneratorConfiguration()?.enable
=== true) {
2288 this.startAutomaticTransactionGenerator();
2290 this.wsConnectionRestarted
=== true && this.flushMessageBuffer();
2293 private async stopMessageSequence(
2294 reason
: StopTransactionReason
= StopTransactionReason
.NONE
2296 // Stop WebSocket ping
2297 this.stopWebSocketPing();
2299 this.stopHeartbeat();
2300 // Stop ongoing transactions
2301 if (this.automaticTransactionGenerator
?.started
=== true) {
2302 this.stopAutomaticTransactionGenerator();
2304 await this.stopRunningTransactions(reason
);
2306 if (this.hasEvses
) {
2307 for (const [evseId
, evseStatus
] of this.evses
) {
2309 for (const [connectorId
, connectorStatus
] of evseStatus
.connectors
) {
2310 await this.ocppRequestService
.requestHandler
<
2311 StatusNotificationRequest
,
2312 StatusNotificationResponse
2315 RequestCommand
.STATUS_NOTIFICATION
,
2316 OCPPServiceUtils
.buildStatusNotificationRequest(
2319 ConnectorStatusEnum
.Unavailable
,
2323 delete connectorStatus
?.status;
2328 for (const connectorId
of this.connectors
.keys()) {
2329 if (connectorId
> 0) {
2330 await this.ocppRequestService
.requestHandler
<
2331 StatusNotificationRequest
,
2332 StatusNotificationResponse
2335 RequestCommand
.STATUS_NOTIFICATION
,
2336 OCPPServiceUtils
.buildStatusNotificationRequest(
2339 ConnectorStatusEnum
.Unavailable
2342 delete this.getConnectorStatus(connectorId
)?.status;
2348 private startWebSocketPing(): void {
2349 const webSocketPingInterval
: number = ChargingStationConfigurationUtils
.getConfigurationKey(
2351 StandardParametersKey
.WebSocketPingInterval
2354 ChargingStationConfigurationUtils
.getConfigurationKey(
2356 StandardParametersKey
.WebSocketPingInterval
2360 if (webSocketPingInterval
> 0 && !this.webSocketPingSetInterval
) {
2361 this.webSocketPingSetInterval
= setInterval(() => {
2362 if (this.isWebSocketConnectionOpened() === true) {
2363 this.wsConnection
?.ping();
2365 }, webSocketPingInterval
* 1000);
2367 `${this.logPrefix()} WebSocket ping started every ${formatDurationSeconds(
2368 webSocketPingInterval
2371 } else if (this.webSocketPingSetInterval
) {
2373 `${this.logPrefix()} WebSocket ping already started every ${formatDurationSeconds(
2374 webSocketPingInterval
2379 `${this.logPrefix()} WebSocket ping interval set to ${webSocketPingInterval}, not starting the WebSocket ping`
2384 private stopWebSocketPing(): void {
2385 if (this.webSocketPingSetInterval
) {
2386 clearInterval(this.webSocketPingSetInterval
);
2387 delete this.webSocketPingSetInterval
;
2391 private getConfiguredSupervisionUrl(): URL
{
2392 let configuredSupervisionUrl
: string;
2393 const supervisionUrls
= this.stationInfo
?.supervisionUrls
?? Configuration
.getSupervisionUrls();
2394 if (isNotEmptyArray(supervisionUrls
)) {
2395 let configuredSupervisionUrlIndex
: number;
2396 switch (Configuration
.getSupervisionUrlDistribution()) {
2397 case SupervisionUrlDistribution
.RANDOM
:
2398 configuredSupervisionUrlIndex
= Math.floor(secureRandom() * supervisionUrls
.length
);
2400 case SupervisionUrlDistribution
.ROUND_ROBIN
:
2401 case SupervisionUrlDistribution
.CHARGING_STATION_AFFINITY
:
2403 Object.values(SupervisionUrlDistribution
).includes(
2404 Configuration
.getSupervisionUrlDistribution()
2407 `${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
2408 SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
2411 configuredSupervisionUrlIndex
= (this.index
- 1) % supervisionUrls
.length
;
2414 configuredSupervisionUrl
= supervisionUrls
[configuredSupervisionUrlIndex
];
2416 configuredSupervisionUrl
= supervisionUrls
as string;
2418 if (isNotEmptyString(configuredSupervisionUrl
)) {
2419 return new URL(configuredSupervisionUrl
);
2421 const errorMsg
= 'No supervision url(s) configured';
2422 logger
.error(`${this.logPrefix()} ${errorMsg}`);
2423 throw new BaseError(`${errorMsg}`);
2426 private stopHeartbeat(): void {
2427 if (this.heartbeatSetInterval
) {
2428 clearInterval(this.heartbeatSetInterval
);
2429 delete this.heartbeatSetInterval
;
2433 private terminateWSConnection(): void {
2434 if (this.isWebSocketConnectionOpened() === true) {
2435 this.wsConnection
?.terminate();
2436 this.wsConnection
= null;
2440 private getReconnectExponentialDelay(): boolean {
2441 return this.stationInfo
?.reconnectExponentialDelay
?? false;
2444 private async reconnect(): Promise
<void> {
2445 // Stop WebSocket ping
2446 this.stopWebSocketPing();
2448 this.stopHeartbeat();
2449 // Stop the ATG if needed
2450 if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure
=== true) {
2451 this.stopAutomaticTransactionGenerator();
2454 this.autoReconnectRetryCount
< this.getAutoReconnectMaxRetries() ||
2455 this.getAutoReconnectMaxRetries() === -1
2457 ++this.autoReconnectRetryCount
;
2458 const reconnectDelay
= this.getReconnectExponentialDelay()
2459 ? exponentialDelay(this.autoReconnectRetryCount
)
2460 : this.getConnectionTimeout() * 1000;
2461 const reconnectDelayWithdraw
= 1000;
2462 const reconnectTimeout
=
2463 reconnectDelay
&& reconnectDelay
- reconnectDelayWithdraw
> 0
2464 ? reconnectDelay
- reconnectDelayWithdraw
2467 `${this.logPrefix()} WebSocket connection retry in ${roundTo(
2470 )}ms, timeout ${reconnectTimeout}ms`
2472 await sleep(reconnectDelay
);
2474 `${this.logPrefix()} WebSocket connection retry #${this.autoReconnectRetryCount.toString()}`
2476 this.openWSConnection(
2478 ...(this.stationInfo
?.wsOptions
?? {}),
2479 handshakeTimeout
: reconnectTimeout
,
2481 { closeOpened
: true }
2483 this.wsConnectionRestarted
= true;
2484 } else if (this.getAutoReconnectMaxRetries() !== -1) {
2486 `${this.logPrefix()} WebSocket connection retries failure: maximum retries reached (${
2487 this.autoReconnectRetryCount
2488 }) or retries disabled (${this.getAutoReconnectMaxRetries()})`