# MTA
*.mta
mta_archives/
+src/scripts/scriptConfig.json
"integrity": "sha1-DGwfq+KyPRcXPZpht7cJPrnhdp4=",
"dev": true
},
+ "bl": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
+ "integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
+ "requires": {
+ "readable-stream": "^2.3.5",
+ "safe-buffer": "^5.1.1"
+ }
+ },
"bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
"pako": "~1.0.5"
}
},
+ "bson": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz",
+ "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg=="
+ },
"buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
+ "denque": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz",
+ "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ=="
+ },
"depcheck": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/depcheck/-/depcheck-0.8.3.tgz",
"safe-buffer": "^5.1.2"
}
},
+ "memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "optional": true
+ },
"meow": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
"integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ=="
},
+ "mongodb": {
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz",
+ "integrity": "sha512-rOZuR0QkodZiM+UbQE5kDsJykBqWi0CL4Ec2i1nrGrUI3KO11r6Fbxskqmq3JK2NH7aW4dcccBuUujAP0ERl5w==",
+ "requires": {
+ "bl": "^2.2.1",
+ "bson": "^1.1.4",
+ "denque": "^1.4.1",
+ "require_optional": "^1.0.1",
+ "safe-buffer": "^5.1.2",
+ "saslprep": "^1.0.0"
+ }
+ },
"morphdom": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/morphdom/-/morphdom-2.6.1.tgz",
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
- "dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"integrity": "sha1-wR6XJ2tluOKSP3Xav1+y7ww4Qbk=",
"dev": true
},
+ "require_optional": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz",
+ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==",
+ "requires": {
+ "resolve-from": "^2.0.0",
+ "semver": "^5.1.0"
+ },
+ "dependencies": {
+ "resolve-from": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz",
+ "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
+ }
+ }
+ },
"resolve": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"dev": true
},
+ "saslprep": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
+ "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
+ "optional": true,
+ "requires": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
"sax": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"semver": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
- "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==",
- "dev": true
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
},
"semver-compare": {
"version": "1.0.0",
"integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
"dev": true
},
+ "sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
+ "optional": true,
+ "requires": {
+ "memory-pager": "^1.0.2"
+ }
+ },
"spdx-correct": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
"git:spush": "git push --recurse-submodules=on-demand"
},
"dependencies": {
+ "mongodb": "^3.6.3",
"source-map-support": "^0.5.19",
"tslib": "^2.1.0",
"uuid": "^8.3.2",
"statisticsDisplayInterval": 60,
"useWorkerPool": false,
"workerPoolSize": 16,
+ "chargingStationsPerWorker": 1,
+ "chargingStationIdSuffix": "",
"stationTemplateURLs": [
{
"file": "./src/assets/station-templates/siemens.station-template.json",
} else {
startResponse = await this.startTransaction(connectorId, this);
}
- if (startResponse.idTagInfo.status !== AuthorizationStatus.ACCEPTED) {
+ if (startResponse?.idTagInfo?.status !== AuthorizationStatus.ACCEPTED) {
logger.info(this._logPrefix(connectorId) + ' transaction rejected');
await Utils.sleep(Constants.CHARGING_STATION_ATG_WAIT_TIME);
} else {
if (self._chargingStation.hasAuthorizedTags()) {
const tagId = self._chargingStation.getRandomTagId();
logger.info(self._logPrefix(connectorId) + ' start transaction for tagID ' + tagId);
- return await self._chargingStation.sendStartTransaction(connectorId, tagId);
+ // Authorize tagId
+ const authorizeResponse = await self._chargingStation.sendAuthorize(tagId);
+ if (authorizeResponse?.idTagInfo?.status === AuthorizationStatus.ACCEPTED) {
+ // Start transaction
+ return await self._chargingStation.sendStartTransaction(connectorId, tagId);
+ } else {
+ return authorizeResponse as StartTransactionResponse;
+ }
}
logger.info(self._logPrefix(connectorId) + ' start transaction without a tagID');
return await self._chargingStation.sendStartTransaction(connectorId);
-import { AuthorizationStatus, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
+import { AuthorizationStatus, AuthorizeRequest, AuthorizeResponse, StartTransactionRequest, StartTransactionResponse, StopTransactionReason, StopTransactionRequest, StopTransactionResponse } from '../types/ocpp/1.6/Transaction';
import { AvailabilityType, BootNotificationRequest, ChangeAvailabilityRequest, ChangeConfigurationRequest, GetConfigurationRequest, HeartbeatRequest, IncomingRequestCommand, RemoteStartTransactionRequest, RemoteStopTransactionRequest, RequestCommand, ResetRequest, SetChargingProfileRequest, StatusNotificationRequest, UnlockConnectorRequest } from '../types/ocpp/1.6/Requests';
import { BootNotificationResponse, ChangeAvailabilityResponse, ChangeConfigurationResponse, DefaultResponse, GetConfigurationResponse, HeartbeatResponse, RegistrationStatus, SetChargingProfileResponse, StatusNotificationResponse, UnlockConnectorResponse } from '../types/ocpp/1.6/RequestResponses';
import { ChargingProfile, ChargingProfilePurposeType } from '../types/ocpp/1.6/ChargingProfile';
}
_getStationName(stationTemplate: ChargingStationTemplate): string {
- return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4);
+ // In case of multiple instances: add instance index to charging station id
+ let instanceIndex = process.env.CF_INSTANCE_INDEX ? process.env.CF_INSTANCE_INDEX : 0;
+ instanceIndex = instanceIndex > 0 ? instanceIndex : '';
+
+ const idSuffix = Configuration.getChargingStationIdSuffix();
+
+ return stationTemplate.fixedName ? stationTemplate.baseName : stationTemplate.baseName + '-' + instanceIndex + ('000000000' + this._index.toString()).substr(('000000000' + this._index.toString()).length - 4) + idSuffix;
}
_buildStationInfo(): ChargingStationInfo {
}
_startAuthorizationFileMonitoring(): void {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- fs.watchFile(this._getAuthorizationFile(), (current, previous) => {
+ fs.watch(this._getAuthorizationFile()).on("change", e => {
try {
logger.debug(this._logPrefix() + ' Authorization file ' + this._getAuthorizationFile() + ' have changed, reload');
// Initialize _authorizedTags
}
_startStationTemplateFileMonitoring(): void {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- fs.watchFile(this._stationTemplateFile, (current, previous) => {
+ fs.watch(this._stationTemplateFile).on("change", e => {
try {
logger.debug(this._logPrefix() + ' Template file ' + this._stationTemplateFile + ' have changed, reload');
// Initialize
this._initialize();
+ // Stop the ATG
if (!this._stationInfo.AutomaticTransactionGenerator.enable &&
this._automaticTransactionGeneration) {
this._automaticTransactionGeneration.stop().catch(() => { });
}
+ // Start the ATG
+ if (this._stationInfo.AutomaticTransactionGenerator.enable) {
+ if (!this._automaticTransactionGeneration) {
+ this._automaticTransactionGeneration = new AutomaticTransactionGenerator(this);
+ }
+ if (this._automaticTransactionGeneration.timeToStop) {
+ this._automaticTransactionGeneration.start();
+ }
+ }
// FIXME?: restart heartbeat and WebSocket ping when their interval values have changed
} catch (error) {
logger.error(this._logPrefix() + ' Charging station template file monitoring error: %j', error);
}
}
+ async sendAuthorize(idTag?: string): Promise<AuthorizeResponse> {
+ try {
+ const payload: AuthorizeRequest = {
+ ...!Utils.isUndefined(idTag) ? { idTag } : { idTag: Constants.TRANSACTION_DEFAULT_IDTAG },
+ };
+ return await this.sendMessage(Utils.generateUUID(), payload, MessageType.CALL_MESSAGE, RequestCommand.AUTHORIZE) as AuthorizeResponse;
+ } catch (error) {
+ this.handleRequestError(RequestCommand.AUTHORIZE, error);
+ }
+ }
+
async sendStartTransaction(connectorId: number, idTag?: string): Promise<StartTransactionResponse> {
try {
const payload: StartTransactionRequest = {
-import { isMainThread, workerData } from 'worker_threads';
+import { isMainThread, parentPort, workerData } from 'worker_threads';
+import Constants from '../utils/Constants';
import ChargingStation from './ChargingStation';
if (!isMainThread) {
const station = new ChargingStation(workerData.index as number, workerData.templateFile as string);
station.start();
+
+ // Listener: start new charging station from main thread
+ addListener();
+}
+
+function addListener() {
+ parentPort.setMaxListeners(1000);
+ parentPort.on("message", e => {
+ if (e.id === Constants.START_NEW_CHARGING_STATION) {
+ startChargingStation(e.workerData);
+ }
+ });
+}
+
+function startChargingStation(data: any) {
+ const station = new ChargingStation(data.index as number, data.templateFile as string);
+ station.start();
}
import Configuration from '../utils/Configuration';
import Pool from 'worker-threads-pool';
import WorkerData from '../types/WorkerData';
+import Constants from '../utils/Constants';
export default class Wrk {
private _workerScript: string;
private _workerData: WorkerData;
private _index: number;
private _concurrentWorkers: number;
+ private _worker: Worker;
/**
* Create a new `Wrk`.
* @return {Promise}
* @public
*/
- async start(): Promise<unknown> {
+ async start(): Promise<Worker> {
if (Configuration.useWorkerPool()) {
- return this._startWorkerWithPool();
+ this._startWorkerWithPool();
+ } else {
+ this._startWorker();
}
- return this._startWorker();
+ return this._worker;
+ }
+
+ /**
+ *
+ * @return {Promise}
+ * @public
+ */
+ async startNewChargingStation(workerData: WorkerData, numConcurrentWorkers: number): Promise<void> {
+ this._workerData = workerData;
+ this._index = workerData.index;
+ this._concurrentWorkers = numConcurrentWorkers;
+ this._worker.postMessage({ id : Constants.START_NEW_CHARGING_STATION, workerData: workerData });
}
/**
}
worker.once('message', resolve);
worker.once('error', reject);
+ this._worker = worker;
});
});
}
reject(new Error(`Worker id ${this._index} stopped with exit code ${code}`));
}
});
+ this._worker = worker;
});
}
}
private constructor() { }
public static getInstance(): Pool {
- if (!WorkerPool._instance) {
+ if (!WorkerPool._instance || (WorkerPool._instance?.size === WorkerPool.concurrentWorkers)) {
WorkerPool._instance = new Pool({ max: WorkerPool.concurrentWorkers });
}
return WorkerPool._instance;
--- /dev/null
+var MongoClient = require('mongodb');
+var fs = require('fs');
+
+// This script deletes charging stations
+// Filter charging stations by id pattern
+
+// Use Case: ev-simulator creates thousands of charging stations, which are not longer needed.
+// Delete these charging stations all at once
+
+// Config
+var config = JSON.parse(fs.readFileSync('scriptConfig.json', 'utf8'));
+
+// Mongo Connection and Query
+if (config && config.mongoConnectionString) {
+ MongoClient.connect(config.mongoConnectionString, {
+ useUnifiedTopology: true,
+ useNewUrlParser: true
+ }, async function(err, client) {
+ const db = client.db('evse');
+
+ for await (const tenantID of config.tenantIDs) {
+ let response = await db.collection(tenantID + '.chargingstations').deleteMany(
+ { _id: {'$regex': config.idPattern} }
+ );
+ console.log(response.deletedCount, `Charging Stations with id = %${config.idPattern}% deleted. TenantID =`, tenantID);
+ }
+ client.close();
+ });
+}
--- /dev/null
+{
+ "publicFlag": true,
+ "tenantIDs": [""],
+ "idPattern": "",
+ "mongoConnectionString": "mongodb://..."
+}
--- /dev/null
+var MongoClient = require('mongodb');
+var fs = require('fs');
+
+// This script sets charging stations public or private
+// Filter charging stations by id pattern
+
+// Use case: simulate charging station for roaming tests
+// charging stations are private by default
+// set public = true
+
+// Config
+var config = JSON.parse(fs.readFileSync('scriptConfig.json', 'utf8'));
+
+// Mongo Connection and Query
+if (config && config.mongoConnectionString) {
+ MongoClient.connect(config.mongoConnectionString, {
+ useUnifiedTopology: true,
+ useNewUrlParser: true
+ }, async function(err, client) {
+ const db = client.db('evse');
+
+ for await (const tenantID of config.tenantIDs) {
+ let response = await db.collection(tenantID + '.chargingstations').updateMany(
+ { _id: {'$regex': config.idPattern} },
+ { $set: { public : config.publicFlag } }
+ );
+ console.log(response.modifiedCount, `Charging Stations with id = %${config.idPattern}% updated. TenantID =`, tenantID);
+ }
+ client.close();
+ });
+}
import Configuration from './utils/Configuration';
import { StationTemplateURL } from './types/ConfigurationData';
+import Utils from './utils/Utils';
import Wrk from './charging-station/Worker';
+import WorkerData from './types/WorkerData';
+import fs from 'fs';
class Bootstrap {
- static start() {
+ static async start() {
try {
let numStationsTotal = 0;
let numConcurrentWorkers = 0;
+ let worker: Wrk;
+ let chargingStationsPerWorker = Configuration.getChargingStationsPerWorker();
+ let counter = 0;
// Start each ChargingStation object in a worker thread
if (Configuration.getStationTemplateURLs()) {
- Configuration.getStationTemplateURLs().forEach((stationURL: StationTemplateURL) => {
+ for await (const stationURL of Configuration.getStationTemplateURLs()) {
try {
const nbStations = stationURL.numberOfStations ? stationURL.numberOfStations : 0;
numStationsTotal += nbStations;
for (let index = 1; index <= nbStations; index++) {
- const worker = new Wrk('./dist/charging-station/StationWorker.js', {
+ const workerData = {
index,
- templateFile: stationURL.file,
- }, numStationsTotal);
- worker.start().catch(() => {});
+ templateFile: stationURL.file
+ } as WorkerData;
+ if(counter === 0 || counter === chargingStationsPerWorker) {
+ // Start new worker with one charging station
+ worker = await new Wrk('./dist/charging-station/StationWorker.js', workerData, numStationsTotal);
+ worker.start().catch(() => {});
+ counter = 0;
+ // Start workers sequentially to optimize memory at start time
+ await Utils.sleep(500);
+ } else {
+ // Add new charging station to existing Worker
+ worker.startNewChargingStation(workerData, numStationsTotal)
+ }
+ counter++;
+ // Start charging station sequentially to optimize memory at start time
numConcurrentWorkers = worker.concurrentWorkers;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log('Charging station start with template file ' + stationURL.file + ' error ' + JSON.stringify(error, null, ' '));
}
- });
+ }
} else {
console.log('No stationTemplateURLs defined in configuration, exiting');
}
distributeStationsToTenantsEqually?: boolean;
useWorkerPool?: boolean;
workerPoolSize?: number;
+ chargingStationsPerWorker: number;
+ chargingStationIdSuffix: string;
logFormat?: string;
logLevel?: string;
logRotate?: boolean;
HEARTBEAT = 'Heartbeat',
STATUS_NOTIFICATION = 'StatusNotification',
CHANGE_CONFIGURATION = 'ChangeConfiguration',
+ AUTHORIZE = 'Authorize',
START_TRANSACTION = 'StartTransaction',
STOP_TRANSACTION = 'StopTransaction',
METERVALUES = 'MeterValues'
expiryDate?: Date;
}
+export interface AuthorizeRequest {
+ idTag: string;
+
+}
+export interface AuthorizeResponse {
+ idTagInfo: IdTagInfo;
+}
+
export interface StartTransactionRequest {
connectorId: number;
idTag: string;
return Configuration.getConfig().workerPoolSize;
}
+ static getChargingStationsPerWorker(): number {
+ return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'chargingStationsPerWorker') ? Configuration.getConfig().chargingStationsPerWorker : 1;
+ }
+
+ static getChargingStationIdSuffix(): string {
+ return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'chargingStationIdSuffix') ? Configuration.getConfig().chargingStationIdSuffix : '';
+ }
+
static getLogConsole(): boolean {
Configuration.deprecateConfigurationKey('consoleLog', 'Use \'logConsole\' instead');
return Configuration.objectHasOwnProperty(Configuration.getConfig(), 'logConsole') ? Configuration.getConfig().logConsole : false;
static readonly CHARGING_STATION_ATG_WAIT_TIME = 2000; // Ms
static readonly TRANSACTION_DEFAULT_IDTAG = '00000000';
+
+ static readonly START_NEW_CHARGING_STATION = 'startNewChargingStation';
}