// 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 { 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';
isUndefined,
logPrefix,
logger,
+ min,
+ once,
roundTo,
secureRandom,
sleep,
- watchJsonFile,
+ // watchJsonFile,
} from '../utils';
export class ChargingStation {
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>;
this.started = false;
this.starting = false;
this.stopping = false;
+ this.wsConnection = null;
this.wsConnectionRestarted = false;
this.autoReconnectRetryCount = 0;
this.index = index;
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;
}
}
this.openWSConnection();
// Monitor charging station template file
- this.templateFileWatcher = watchJsonFile(
- this.templateFile,
- FileType.ChargingStationTemplate,
- this.logPrefix(),
- undefined,
- (event, filename): void => {
- if (isNotEmptyString(filename) && event === 'change') {
- try {
- logger.debug(
- `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
- this.templateFile
- } file have changed, reload`,
- );
- this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
- // Initialize
- this.initialize();
- this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
- // Restart the ATG
- this.stopAutomaticTransactionGenerator();
- delete this.automaticTransactionGeneratorConfiguration;
- if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
- this.startAutomaticTransactionGenerator();
- }
- if (this.getEnableStatistics() === true) {
- this.performanceStatistics?.restart();
- } else {
- this.performanceStatistics?.stop();
- }
- // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
- } catch (error) {
- logger.error(
- `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
- error,
- );
- }
- }
- },
- );
+ // FIXME: Disabled until the spurious configuration file change detection is identified
+ // this.templateFileWatcher = watchJsonFile(
+ // this.templateFile,
+ // FileType.ChargingStationTemplate,
+ // this.logPrefix(),
+ // undefined,
+ // (event, filename): void => {
+ // if (isNotEmptyString(filename) && event === 'change') {
+ // try {
+ // logger.debug(
+ // `${this.logPrefix()} ${FileType.ChargingStationTemplate} ${
+ // this.templateFile
+ // } file have changed, reload`,
+ // );
+ // this.sharedLRUCache.deleteChargingStationTemplate(this.templateFileHash);
+ // // Initialize
+ // this.initialize();
+ // this.idTagsCache.deleteIdTags(getIdTagsFile(this.stationInfo)!);
+ // // Restart the ATG
+ // this.stopAutomaticTransactionGenerator()
+ // .then(() => {
+ // delete this.automaticTransactionGeneratorConfiguration;
+ // if (this.getAutomaticTransactionGeneratorConfiguration()?.enable === true) {
+ // this.startAutomaticTransactionGenerator();
+ // }
+ // })
+ // .catch((err) =>
+ // logger.error(
+ // `${this.logPrefix()} failed to stop ATG at ${
+ // FileType.ChargingStationTemplate
+ // } reload`,
+ // err,
+ // ),
+ // );
+ // if (this.getEnableStatistics() === true) {
+ // this.performanceStatistics?.restart();
+ // } else {
+ // this.performanceStatistics?.stop();
+ // }
+ // // FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
+ // } catch (error) {
+ // logger.error(
+ // `${this.logPrefix()} ${FileType.ChargingStationTemplate} file monitoring error:`,
+ // error,
+ // );
+ // }
+ // }
+ // },
+ // );
this.started = true;
parentPort?.postMessage(buildStartedMessage(this));
this.starting = false;
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;
}
parentPort?.postMessage(buildUpdatedMessage(this));
}
- public stopAutomaticTransactionGenerator(connectorIds?: number[]): void {
+ public async stopAutomaticTransactionGenerator(connectorIds?: number[]): Promise<void> {
if (isNotEmptyArray(connectorIds)) {
for (const connectorId of connectorIds!) {
- this.automaticTransactionGenerator?.stopConnector(connectorId);
+ await this.automaticTransactionGenerator?.stopConnector(connectorId);
}
} else {
- this.automaticTransactionGenerator?.stop();
+ await this.automaticTransactionGenerator?.stop();
}
this.saveAutomaticTransactionGeneratorConfiguration();
parentPort?.postMessage(buildUpdatedMessage(this));
}
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);
}
)
.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 ${
// -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;
}
this.stopHeartbeat();
// Stop ongoing transactions
if (this.automaticTransactionGenerator?.started === true) {
- this.stopAutomaticTransactionGenerator();
+ await this.stopAutomaticTransactionGenerator();
} else {
await this.stopRunningTransactions(reason);
}
this.stopHeartbeat();
// Stop the ATG if needed
if (this.getAutomaticTransactionGeneratorConfiguration().stopOnConnectionFailure === true) {
- this.stopAutomaticTransactionGenerator();
+ await this.stopAutomaticTransactionGenerator();
}
if (
this.autoReconnectRetryCount < this.getAutoReconnectMaxRetries()! ||