| Key | Value(s) | Default Value | Value type | Description |
| -------------------------- | ------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| supervisionUrls | | [] | string \| string[] | string or array of global connection URIs to OCPP-J servers |
-| supervisionUrlDistribution | round-robin/random/sequential | round-robin | boolean | supervision urls distribution policy to simulated charging stations |
+| supervisionUrlDistribution | round-robin/random/charging-station-affinity | charging-station-affinity | boolean | supervision urls distribution policy to simulated charging stations |
| logStatisticsInterval | | 60 | integer | seconds between charging stations statistics output in the logs |
| logConsole | true/false | false | boolean | output logs on the console |
| logFormat | | simple | string | winston log format |
#### AutomaticTransactionGenerator section
+Section type definition:
+
+```ts
+type AutomaticTransactionGeneratorConfiguration = {
+ enable: boolean;
+ minDuration: number;
+ maxDuration: number;
+ minDelayBetweenTwoTransactions: number;
+ maxDelayBetweenTwoTransactions: number;
+ probabilityOfStart: number;
+ stopAfterHours: number;
+ stopOnConnectionFailure: boolean;
+ requireAuthorize?: boolean;
+ idTagDistribution?: 'random' | 'round-robin' | 'connector-affinity';
+};
+```
+
+Section example:
+
```json
"AutomaticTransactionGenerator": {
"enable": false,
"probabilityOfStart": 1,
"stopAfterHours": 0.3,
"stopOnConnectionFailure": true,
- "requireAuthorize": true
+ "requireAuthorize": true,
+ "idTagDistribution": "random"
}
```
import BaseError from '../exception/BaseError';
import PerformanceStatistics from '../performance/PerformanceStatistics';
-import type {
- AutomaticTransactionGeneratorConfiguration,
- Status,
+import {
+ type AutomaticTransactionGeneratorConfiguration,
+ IdTagDistribution,
+ type Status,
} from '../types/AutomaticTransactionGenerator';
import { RequestCommand } from '../types/ocpp/Requests';
import {
import logger from '../utils/Logger';
import Utils from '../utils/Utils';
import type ChargingStation from './ChargingStation';
+import { ChargingStationUtils } from './ChargingStationUtils';
export default class AutomaticTransactionGenerator {
private static readonly instances: Map<string, AutomaticTransactionGenerator> = new Map<
public readonly configuration: AutomaticTransactionGeneratorConfiguration;
public started: boolean;
private readonly chargingStation: ChargingStation;
+ private idTagIndex: number;
private constructor(
automaticTransactionGeneratorConfiguration: AutomaticTransactionGeneratorConfiguration,
this.started = false;
this.configuration = automaticTransactionGeneratorConfiguration;
this.chargingStation = chargingStation;
+ this.idTagIndex = 0;
this.connectorsStatus = new Map<number, Status>();
this.initializeConnectorsStatus();
}
const beginId = PerformanceStatistics.beginMeasure(measureId);
let startResponse: StartTransactionResponse;
if (this.chargingStation.hasAuthorizedTags()) {
- const idTag = this.chargingStation.getRandomIdTag();
+ const idTag = this.getIdTag(connectorId);
const startTransactionLogMsg = `${this.logPrefix(
connectorId
)} start transaction with an idTag '${idTag}'`;
return this.configuration?.requireAuthorize ?? true;
}
+ private getRandomIdTag(authorizationFile: string): string {
+ const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+ this.idTagIndex = Math.floor(Utils.secureRandom() * tags.length);
+ return tags[this.idTagIndex];
+ }
+
+ private getRoundRobinIdTag(authorizationFile: string): string {
+ const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+ const idTag = tags[this.idTagIndex];
+ this.idTagIndex = this.idTagIndex === tags.length - 1 ? 0 : this.idTagIndex + 1;
+ return idTag;
+ }
+
+ private getConnectorAffinityIdTag(authorizationFile: string, connectorId: number): string {
+ const tags = this.chargingStation.authorizedTagsCache.getAuthorizedTags(authorizationFile);
+ this.idTagIndex = (this.chargingStation.index - 1 + (connectorId - 1)) % tags.length;
+ return tags[this.idTagIndex];
+ }
+
+ private getIdTag(connectorId: number): string {
+ const authorizationFile = ChargingStationUtils.getAuthorizationFile(
+ this.chargingStation.stationInfo
+ );
+ switch (this.configuration?.idTagDistribution) {
+ case IdTagDistribution.RANDOM:
+ return this.getRandomIdTag(authorizationFile);
+ case IdTagDistribution.ROUND_ROBIN:
+ return this.getRoundRobinIdTag(authorizationFile);
+ case IdTagDistribution.CONNECTOR_AFFINITY:
+ return this.getConnectorAffinityIdTag(authorizationFile, connectorId);
+ default:
+ return this.getRoundRobinIdTag(authorizationFile);
+ }
+ }
+
private logPrefix(connectorId?: number): string {
return Utils.logPrefix(
` ${this.chargingStation.stationInfo.chargingStationId} | ATG${
import path from 'path';
import { fileURLToPath } from 'url';
-import { isMainThread } from 'worker_threads';
+import { type Worker, isMainThread } from 'worker_threads';
import chalk from 'chalk';
workerChoiceStrategy: Configuration.getWorker().poolStrategy,
},
messageHandler: this.messageHandler.bind(this) as (
+ this: Worker,
msg: ChargingStationWorkerMessage<ChargingStationWorkerMessageData>
) => void,
}
import SharedLRUCache from './SharedLRUCache';
export default class ChargingStation {
+ public readonly index: number;
public readonly templateFile: string;
public stationInfo!: ChargingStationInfo;
public started: boolean;
public powerDivider!: number;
private starting: boolean;
private stopping: boolean;
- private readonly index: number;
private configurationFile!: string;
private configurationFileHash!: string;
private connectorsConfigurationHash!: string;
private ocppIncomingRequestService!: OCPPIncomingRequestService;
private readonly messageBuffer: Set<string>;
private configuredSupervisionUrl!: URL;
+ private configuredSupervisionUrlIndex!: number;
private wsConnectionRestarted: boolean;
private autoReconnectRetryCount: number;
private templateFileWatcher!: fs.FSWatcher;
);
}
- public getRandomIdTag(): string {
- const authorizationFile = ChargingStationUtils.getAuthorizationFile(this.stationInfo);
- const index = Math.floor(
- Utils.secureRandom() * this.authorizedTagsCache.getAuthorizedTags(authorizationFile).length
- );
- return this.authorizedTagsCache.getAuthorizedTags(authorizationFile)[index];
- }
-
public hasAuthorizedTags(): boolean {
return !Utils.isEmptyArray(
this.authorizedTagsCache.getAuthorizedTags(
stationInfo.resetTime = stationTemplate.resetTime
? stationTemplate.resetTime * 1000
: Constants.CHARGING_STATION_DEFAULT_RESET_TIME;
- const configuredMaxConnectors = ChargingStationUtils.getConfiguredNumberOfConnectors(
- this.index,
- stationTemplate
- );
+ const configuredMaxConnectors =
+ ChargingStationUtils.getConfiguredNumberOfConnectors(stationTemplate);
ChargingStationUtils.checkConfiguredMaxConnectors(
configuredMaxConnectors,
this.templateFile,
}
// 0 for disabling
- private getConnectionTimeout(): number | undefined {
+ private getConnectionTimeout(): number {
if (
ChargingStationConfigurationUtils.getConfigurationKey(
this,
}
// -1 for unlimited, 0 for disabling
- private getAutoReconnectMaxRetries(): number | undefined {
+ private getAutoReconnectMaxRetries(): number {
if (!Utils.isUndefined(this.stationInfo.autoReconnectMaxRetries)) {
return this.stationInfo.autoReconnectMaxRetries;
}
}
// 0 for disabling
- private getRegistrationMaxRetries(): number | undefined {
+ private getRegistrationMaxRetries(): number {
if (!Utils.isUndefined(this.stationInfo.registrationMaxRetries)) {
return this.stationInfo.registrationMaxRetries;
}
this.stationInfo.supervisionUrls ?? Configuration.getSupervisionUrls()
);
if (!Utils.isEmptyArray(supervisionUrls)) {
- let urlIndex = 0;
switch (Configuration.getSupervisionUrlDistribution()) {
case SupervisionUrlDistribution.ROUND_ROBIN:
- urlIndex = (this.index - 1) % supervisionUrls.length;
+ // FIXME
+ this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
break;
case SupervisionUrlDistribution.RANDOM:
- // Get a random url
- urlIndex = Math.floor(Utils.secureRandom() * supervisionUrls.length);
+ this.configuredSupervisionUrlIndex = Math.floor(
+ Utils.secureRandom() * supervisionUrls.length
+ );
break;
- case SupervisionUrlDistribution.SEQUENTIAL:
- if (this.index <= supervisionUrls.length) {
- urlIndex = this.index - 1;
- } else {
- logger.warn(
- `${this.logPrefix()} No more configured supervision urls available, using the first one`
- );
- }
+ case SupervisionUrlDistribution.CHARGING_STATION_AFFINITY:
+ this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
break;
default:
logger.error(
`${this.logPrefix()} Unknown supervision url distribution '${Configuration.getSupervisionUrlDistribution()}' from values '${SupervisionUrlDistribution.toString()}', defaulting to ${
- SupervisionUrlDistribution.ROUND_ROBIN
+ SupervisionUrlDistribution.CHARGING_STATION_AFFINITY
}`
);
- urlIndex = (this.index - 1) % supervisionUrls.length;
+ this.configuredSupervisionUrlIndex = (this.index - 1) % supervisionUrls.length;
break;
}
- return new URL(supervisionUrls[urlIndex]);
+ return new URL(supervisionUrls[this.configuredSupervisionUrlIndex]);
}
return new URL(supervisionUrls as string);
}
- private getHeartbeatInterval(): number | undefined {
+ private getHeartbeatInterval(): number {
const HeartbeatInterval = ChargingStationConfigurationUtils.getConfigurationKey(
this,
StandardParametersKey.HeartbeatInterval
}
}
- private getReconnectExponentialDelay(): boolean | undefined {
+ private getReconnectExponentialDelay(): boolean {
return !Utils.isUndefined(this.stationInfo.reconnectExponentialDelay)
? this.stationInfo.reconnectExponentialDelay
: false;
}
}
- public static getConfiguredNumberOfConnectors(
- index: number,
- stationTemplate: ChargingStationTemplate
- ): number {
+ public static getConfiguredNumberOfConnectors(stationTemplate: ChargingStationTemplate): number {
let configuredMaxConnectors: number;
- if (!Utils.isEmptyArray(stationTemplate.numberOfConnectors)) {
+ if (Utils.isEmptyArray(stationTemplate.numberOfConnectors) === false) {
const numberOfConnectors = stationTemplate.numberOfConnectors as number[];
- // Distribute evenly the number of connectors
- configuredMaxConnectors = numberOfConnectors[(index - 1) % numberOfConnectors.length];
- } else if (!Utils.isUndefined(stationTemplate.numberOfConnectors)) {
+ configuredMaxConnectors =
+ numberOfConnectors[Math.floor(Utils.secureRandom() * numberOfConnectors.length)];
+ } else if (Utils.isUndefined(stationTemplate.numberOfConnectors) === false) {
configuredMaxConnectors = stationTemplate.numberOfConnectors as number;
} else {
configuredMaxConnectors = stationTemplate?.Connectors[0]
import { ThreadWorker } from 'poolifier';
-import {
- ChargingStationWorkerData,
- ChargingStationWorkerMessage,
- ChargingStationWorkerMessageEvents,
-} from '../types/ChargingStationWorker';
+import type { ChargingStationWorkerData } from '../types/ChargingStationWorker';
+import { WorkerMessage, WorkerMessageEvents } from '../types/Worker';
import Utils from '../utils/Utils';
import WorkerConstants from '../worker/WorkerConstants';
import ChargingStation from './ChargingStation';
* Listen messages send by the main thread
*/
function addMessageListener(): void {
- parentPort?.on('message', (message: ChargingStationWorkerMessage<ChargingStationWorkerData>) => {
- if (message.id === ChargingStationWorkerMessageEvents.START_WORKER_ELEMENT) {
+ parentPort?.on('message', (message: WorkerMessage<ChargingStationWorkerData>) => {
+ if (message.id === WorkerMessageEvents.START_WORKER_ELEMENT) {
startChargingStation(message.data);
}
});
({ transactionSetInterval, ...connectorStatusRest }) => connectorStatusRest
),
...(chargingStation.automaticTransactionGenerator && {
- automaticTransactionGeneratorStatuses: [
- ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(),
- ],
+ automaticTransactionGenerator: {
+ automaticTransactionGenerator:
+ chargingStation.automaticTransactionGenerator.configuration,
+ automaticTransactionGeneratorStatuses: [
+ ...chargingStation.automaticTransactionGenerator.connectorsStatus.values(),
+ ],
+ },
}),
};
}
+export enum IdTagDistribution {
+ RANDOM = 'random',
+ ROUND_ROBIN = 'round-robin',
+ CONNECTOR_AFFINITY = 'connector-affinity',
+}
+
export type AutomaticTransactionGeneratorConfiguration = {
enable: boolean;
minDuration: number;
stopAfterHours: number;
stopOnConnectionFailure: boolean;
requireAuthorize?: boolean;
+ idTagDistribution?: IdTagDistribution;
};
export type Status = {
export type ChargingStationAutomaticTransactionGeneratorConfiguration = {
automaticTransactionGenerator?: AutomaticTransactionGeneratorConfiguration;
- automaticTransactionGeneratorStatus?: Status;
+ automaticTransactionGeneratorStatuses?: Status[];
};
import type { WebSocket } from 'ws';
-import type { Status } from './AutomaticTransactionGenerator';
+import type { ChargingStationAutomaticTransactionGeneratorConfiguration } from './AutomaticTransactionGenerator';
import type { ChargingStationInfo } from './ChargingStationInfo';
import type { ConnectorStatus } from './ConnectorStatus';
import type { JsonObject } from './JsonType';
| typeof WebSocket.CLOSED;
bootNotificationResponse: BootNotificationResponse;
connectors: ConnectorStatus[];
- automaticTransactionGeneratorStatuses?: Status[];
+ automaticTransactionGenerator?: ChargingStationAutomaticTransactionGeneratorConfiguration;
}
enum ChargingStationMessageEvents {
export type ChargingStationWorkerMessageData = ChargingStationData | Statistics;
-export type ChargingStationWorkerMessage<T extends WorkerData> = Omit<WorkerMessage<T>, 'id'> & {
+export type ChargingStationWorkerMessage<T extends ChargingStationWorkerMessageData> = Omit<
+ WorkerMessage<T>,
+ 'id'
+> & {
id: ChargingStationWorkerMessageEvents;
};
export enum SupervisionUrlDistribution {
ROUND_ROBIN = 'round-robin',
RANDOM = 'random',
- SEQUENTIAL = 'sequential',
+ CHARGING_STATION_AFFINITY = 'charging-station-affinity',
}
export type StationTemplateUrl = {
poolMinSize?: number;
elementsPerWorker?: number;
poolOptions?: PoolOptions<Worker>;
- messageHandler?: (message: unknown) => void | Promise<void>;
+ messageHandler?: (this: Worker, message: unknown) => void | Promise<void>;
};
export type WorkerData = Record<string, unknown>;