// Partial Copyright Jerome Benoit. 2021-2023. All Rights Reserved.
import { createHash } from 'node:crypto';
-import {
- type FSWatcher,
- closeSync,
- existsSync,
- mkdirSync,
- openSync,
- readFileSync,
- writeFileSync,
-} from 'node:fs';
+import { EventEmitter } from 'node:events';
+import { type FSWatcher, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { URL } from 'node:url';
import { parentPort } from 'node:worker_threads';
type BootNotificationResponse,
type CachedRequest,
type ChargingStationConfiguration,
+ ChargingStationEvents,
type ChargingStationInfo,
type ChargingStationOcppConfiguration,
type ChargingStationTemplate,
isUndefined,
logPrefix,
logger,
+ min,
+ once,
roundTo,
secureRandom,
sleep,
watchJsonFile,
} from '../utils';
-export class ChargingStation {
+export class ChargingStation extends EventEmitter {
public readonly index: number;
public readonly templateFile: string;
public stationInfo!: ChargingStationInfo;
public idTagsCache: IdTagsCache;
public automaticTransactionGenerator!: AutomaticTransactionGenerator | undefined;
public ocppConfiguration!: ChargingStationOcppConfiguration | undefined;
- public wsConnection!: WebSocket | null;
+ public wsConnection: WebSocket | null;
public readonly connectors: Map<number, ConnectorStatus>;
public readonly evses: Map<number, EvseStatus>;
public readonly requests: Map<string, CachedRequest>;
private reservationExpirationSetInterval?: NodeJS.Timeout;
constructor(index: number, templateFile: string) {
+ super();
this.started = false;
this.starting = false;
this.stopping = false;
+ this.wsConnection = null;
this.wsConnectionRestarted = false;
this.autoReconnectRetryCount = 0;
this.index = index;
this.idTagsCache = IdTagsCache.getInstance();
this.chargingStationWorkerBroadcastChannel = new ChargingStationWorkerBroadcastChannel(this);
+ this.on(ChargingStationEvents.started, () => {
+ parentPort?.postMessage(buildStartedMessage(this));
+ });
+ this.on(ChargingStationEvents.stopped, () => {
+ parentPort?.postMessage(buildStoppedMessage(this));
+ });
+ this.on(ChargingStationEvents.updated, () => {
+ parentPort?.postMessage(buildUpdatedMessage(this));
+ });
+
this.initialize();
}
}
public getMaximumPower(stationInfo?: ChargingStationInfo): number {
- const localStationInfo = stationInfo ?? this.stationInfo;
- // eslint-disable-next-line @typescript-eslint/dot-notation
- return (
- (localStationInfo['maxPower' as keyof ChargingStationInfo] as number) ??
- localStationInfo.maximumPower
- );
+ return (stationInfo ?? this.stationInfo).maximumPower!;
}
public getConnectorMaximumAvailablePower(connectorId: number): number {
const connectorMaximumPower = this.getMaximumPower() / this.powerDivider;
const connectorChargingProfilesPowerLimit =
getChargingStationConnectorChargingProfilesPowerLimit(this, connectorId);
- return Math.min(
+ return min(
isNaN(connectorMaximumPower) ? Infinity : connectorMaximumPower,
isNaN(connectorAmperageLimitationPowerLimit!)
? Infinity
);
} else {
logger.error(
- `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()},
- not starting the heartbeat`,
+ `${this.logPrefix()} Heartbeat interval set to ${this.getHeartbeatInterval()}, not starting the heartbeat`,
);
}
}
}
if (this.getConnectorStatus(connectorId)?.transactionStarted === false) {
logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
- with no transaction started`,
+ `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction started`,
);
return;
} else if (
isNullOrUndefined(this.getConnectorStatus(connectorId)?.transactionId)
) {
logger.error(
- `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId}
- with no transaction id`,
+ `${this.logPrefix()} Trying to start MeterValues on connector id ${connectorId} with no transaction id`,
);
return;
}
// Restart the ATG
this.stopAutomaticTransactionGenerator();
delete this.automaticTransactionGeneratorConfiguration;
- if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+ if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
this.startAutomaticTransactionGenerator();
}
if (this.getEnableStatistics() === true) {
},
);
this.started = true;
- parentPort?.postMessage(buildStartedMessage(this));
+ this.emit(ChargingStationEvents.started);
this.starting = false;
} else {
logger.warn(`${this.logPrefix()} Charging station is already starting...`);
}
}
- public async stop(reason?: StopTransactionReason): Promise<void> {
+ public async stop(reason?: StopTransactionReason, stopTransactions?: boolean): Promise<void> {
if (this.started === true) {
if (this.stopping === false) {
this.stopping = true;
- await this.stopMessageSequence(reason);
+ await this.stopMessageSequence(reason, stopTransactions);
this.closeWSConnection();
if (this.getEnableStatistics() === true) {
this.performanceStatistics?.stop();
delete this.bootNotificationResponse;
this.started = false;
this.saveConfiguration();
- parentPort?.postMessage(buildStoppedMessage(this));
+ this.emit(ChargingStationEvents.stopped);
this.stopping = false;
} else {
logger.warn(`${this.logPrefix()} Charging station is already stopping...`);
if (this.isWebSocketConnectionOpened() === true) {
logger.warn(
- `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()}
- is already opened`,
+ `${this.logPrefix()} OCPP connection to URL ${this.wsConnectionUrl.toString()} is already opened`,
);
return;
}
let automaticTransactionGeneratorConfiguration:
| AutomaticTransactionGeneratorConfiguration
| undefined;
- const automaticTransactionGeneratorConfigurationFromFile =
- this.getConfigurationFromFile()?.automaticTransactionGenerator;
+ const stationTemplate = this.getTemplateFromFile();
+ const stationConfiguration = this.getConfigurationFromFile();
if (
this.getAutomaticTransactionGeneratorPersistentConfiguration() &&
- automaticTransactionGeneratorConfigurationFromFile
+ stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
+ stationConfiguration?.automaticTransactionGenerator
) {
automaticTransactionGeneratorConfiguration =
- automaticTransactionGeneratorConfigurationFromFile;
+ stationConfiguration?.automaticTransactionGenerator;
} else {
- automaticTransactionGeneratorConfiguration =
- this.getTemplateFromFile()?.AutomaticTransactionGenerator;
+ automaticTransactionGeneratorConfiguration = stationTemplate?.AutomaticTransactionGenerator;
}
this.automaticTransactionGeneratorConfiguration = {
...Constants.DEFAULT_ATG_CONFIGURATION,
this.automaticTransactionGenerator?.start();
}
this.saveAutomaticTransactionGeneratorConfiguration();
- parentPort?.postMessage(buildUpdatedMessage(this));
+ this.emit(ChargingStationEvents.updated);
}
public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
this.automaticTransactionGenerator?.stop();
}
this.saveAutomaticTransactionGeneratorConfiguration();
- parentPort?.postMessage(buildUpdatedMessage(this));
+ this.emit(ChargingStationEvents.updated);
}
public async stopTransactionOnConnector(
connectorId: number,
- reason = StopTransactionReason.NONE,
+ reason?: StopTransactionReason,
): Promise<StopTransactionResponse> {
const transactionId = this.getConnectorStatus(connectorId)?.transactionId;
if (
{
transactionId,
meterStop: this.getEnergyActiveImportRegisterByTransactionId(transactionId!, true),
- reason,
+ ...(isNullOrUndefined(reason) && { reason }),
},
);
}
}
private startReservationExpirationSetInterval(customInterval?: number): void {
- const interval =
- customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_OBSERVATION_INTERVAL;
+ const interval = customInterval ?? Constants.DEFAULT_RESERVATION_EXPIRATION_INTERVAL;
if (interval > 0) {
logger.info(
`${this.logPrefix()} Reservation expiration date checks started every ${formatDurationMilliSeconds(
}
private stopReservationExpirationSetInterval(): void {
- if (this.reservationExpirationSetInterval) {
+ if (!isNullOrUndefined(this.reservationExpirationSetInterval)) {
clearInterval(this.reservationExpirationSetInterval);
}
}
private getStationInfoFromTemplate(): ChargingStationInfo {
const stationTemplate: ChargingStationTemplate = this.getTemplateFromFile()!;
checkTemplate(stationTemplate, this.logPrefix(), this.templateFile);
- warnTemplateKeysDeprecation(stationTemplate, this.logPrefix(), this.templateFile);
+ const warnTemplateKeysDeprecationOnce = once(warnTemplateKeysDeprecation, this);
+ warnTemplateKeysDeprecationOnce(stationTemplate, this.logPrefix(), this.templateFile);
if (stationTemplate?.Connectors) {
checkConnectorsConfiguration(stationTemplate, this.logPrefix(), this.templateFile);
}
}
private handleUnsupportedVersion(version: OCPPVersion) {
- const errorMsg = `Unsupported protocol version '${version}' configured
- in template file ${this.templateFile}`;
+ const errorMsg = `Unsupported protocol version '${version}' configured in template file ${this.templateFile}`;
logger.error(`${this.logPrefix()} ${errorMsg}`);
throw new BaseError(errorMsg);
}
dirname(this.templateFile.replace('station-templates', 'configurations')),
`${getHashId(this.index, stationTemplate)}.json`,
);
- const chargingStationConfiguration = this.getConfigurationFromFile();
+ const stationConfiguration = this.getConfigurationFromFile();
if (
- chargingStationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
+ stationConfiguration?.stationInfo?.templateHash === stationTemplate?.templateHash &&
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- (chargingStationConfiguration?.connectorsStatus || chargingStationConfiguration?.evsesStatus)
+ (stationConfiguration?.connectorsStatus || stationConfiguration?.evsesStatus)
) {
- this.initializeConnectorsOrEvsesFromFile(chargingStationConfiguration);
+ this.initializeConnectorsOrEvsesFromFile(stationConfiguration);
} else {
this.initializeConnectorsOrEvsesFromTemplate(stationTemplate);
}
for (let connectorId = 0; connectorId <= configuredMaxConnectors; connectorId++) {
if (
connectorId === 0 &&
- (!stationTemplate?.Connectors[connectorId] ||
+ (!stationTemplate?.Connectors?.[connectorId] ||
this.getUseConnectorId0(stationTemplate) === false)
) {
continue;
stationInfo: configurationData.stationInfo,
configurationKey: configurationData.configurationKey,
automaticTransactionGenerator: configurationData.automaticTransactionGenerator,
+ ...(this.connectors.size > 0 && {
+ connectorsStatus: configurationData.connectorsStatus,
+ }),
+ ...(this.evses.size > 0 && { evsesStatus: configurationData.evsesStatus }),
} as ChargingStationConfiguration),
)
.digest('hex');
if (this.configurationFileHash !== configurationHash) {
- AsyncLock.acquire(AsyncLockType.configuration)
- .then(() => {
- configurationData.configurationHash = configurationHash;
- const measureId = `${FileType.ChargingStationConfiguration} write`;
- const beginId = PerformanceStatistics.beginMeasure(measureId);
- const fileDescriptor = openSync(this.configurationFile, 'w');
- writeFileSync(fileDescriptor, JSON.stringify(configurationData, null, 2), 'utf8');
- closeSync(fileDescriptor);
- PerformanceStatistics.endMeasure(measureId, beginId);
- this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
- this.sharedLRUCache.setChargingStationConfiguration(configurationData);
- this.configurationFileHash = configurationHash;
- })
- .catch((error) => {
- handleFileException(
- this.configurationFile,
- FileType.ChargingStationConfiguration,
- error as NodeJS.ErrnoException,
- this.logPrefix(),
- );
- })
- .finally(() => {
- AsyncLock.release(AsyncLockType.configuration).catch(Constants.EMPTY_FUNCTION);
- });
+ AsyncLock.runExclusive(AsyncLockType.configuration, () => {
+ configurationData.configurationHash = configurationHash;
+ const measureId = `${FileType.ChargingStationConfiguration} write`;
+ const beginId = PerformanceStatistics.beginMeasure(measureId);
+ writeFileSync(
+ this.configurationFile,
+ JSON.stringify(configurationData, undefined, 2),
+ 'utf8',
+ );
+ PerformanceStatistics.endMeasure(measureId, beginId);
+ this.sharedLRUCache.deleteChargingStationConfiguration(this.configurationFileHash);
+ this.sharedLRUCache.setChargingStationConfiguration(configurationData);
+ this.configurationFileHash = configurationHash;
+ }).catch((error) => {
+ handleFileException(
+ this.configurationFile,
+ FileType.ChargingStationConfiguration,
+ error as NodeJS.ErrnoException,
+ this.logPrefix(),
+ );
+ });
} else {
logger.debug(
`${this.logPrefix()} Not saving unchanged charging station configuration file ${
);
}
if (this.isRegistered() === true) {
+ this.emit(ChargingStationEvents.registered);
if (this.inAcceptedState() === true) {
+ this.emit(ChargingStationEvents.accepted);
await this.startMessageSequence();
}
} else {
}
this.wsConnectionRestarted = false;
this.autoReconnectRetryCount = 0;
- parentPort?.postMessage(buildUpdatedMessage(this));
+ this.emit(ChargingStationEvents.updated);
} else {
logger.warn(
`${this.logPrefix()} Connection to OCPP server through ${this.wsConnectionUrl.toString()} failed`,
this.started === true && (await this.reconnect());
break;
}
- parentPort?.postMessage(buildUpdatedMessage(this));
+ this.emit(ChargingStationEvents.updated);
}
private getCachedRequest(messageType: MessageType, messageId: string): CachedRequest | undefined {
logger.error(`${this.logPrefix()} ${errorMsg}`);
throw new OCPPError(ErrorType.PROTOCOL_ERROR, errorMsg);
}
- parentPort?.postMessage(buildUpdatedMessage(this));
+ this.emit(ChargingStationEvents.updated);
} else {
throw new OCPPError(
ErrorType.PROTOCOL_ERROR,
return stationTemplate?.useConnectorId0 ?? true;
}
- private async stopRunningTransactions(reason = StopTransactionReason.NONE): Promise<void> {
+ private async stopRunningTransactions(reason?: StopTransactionReason): Promise<void> {
if (this.hasEvses) {
for (const [evseId, evseStatus] of this.evses) {
if (evseId === 0) {
// -1 for unlimited, 0 for disabling
private getAutoReconnectMaxRetries(): number | undefined {
- return (
- this.stationInfo.autoReconnectMaxRetries ?? Configuration.getAutoReconnectMaxRetries() ?? -1
- );
+ return this.stationInfo.autoReconnectMaxRetries ?? -1;
}
- // 0 for disabling
+ // -1 for unlimited, 0 for disabling
private getRegistrationMaxRetries(): number | undefined {
return this.stationInfo.registrationMaxRetries ?? -1;
}
}
// Start the ATG
- if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+ if (this.getAutomaticTransactionGeneratorConfiguration().enable === true) {
this.startAutomaticTransactionGenerator();
}
this.wsConnectionRestarted === true && this.flushMessageBuffer();
}
private async stopMessageSequence(
- reason: StopTransactionReason = StopTransactionReason.NONE,
+ reason?: StopTransactionReason,
+ stopTransactions = this.stationInfo?.stopTransactionsOnStopped ?? true,
): Promise<void> {
// Stop WebSocket ping
this.stopWebSocketPing();
// Stop heartbeat
this.stopHeartbeat();
// Stop ongoing transactions
+ stopTransactions && (await this.stopRunningTransactions(reason));
+ // Stop the ATG
if (this.automaticTransactionGenerator?.started === true) {
this.stopAutomaticTransactionGenerator();
- } else {
- await this.stopRunningTransactions(reason);
}
if (this.hasEvses) {
for (const [evseId, evseStatus] of this.evses) {