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