Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA8ul5QIk9qf+grKDCN1sKPPYsxT4HegfPOm4APr3pISo3rJNy
|
||||
Ns8MfEryn58h8QJ8w7vSh0ZPlLpJq8oY8Y9ckaiJkNIWvRJYfFjwbMeWTLKteCOQ
|
||||
lYTUzKEwxhRmQB1HlKPZYo1SFjgqF7ww4q98YFQQw67gzgs/rpLeEZ4pWzOVn4qM
|
||||
+cBw2B0EgLFER6MI6dVja3t2NkNYRo6Jx0zs+UPKoYci7c9WETFcGOQwv2GjpOrm
|
||||
ekkCLnp7/3/5tmkKmUFbkTZU8+Qoq+Hgj8pAqgRYik2dl9WyE21PHygL7a6psKZR
|
||||
kjvOCPUqCkwjVdf/7oOfp8JAWSnWtyU4sKVU+wIDAQABAoIBAANTmjL9jighVZB3
|
||||
pSE/8Gx0TJmo505PBBH/RqaVUDeBjgChhktk231qQ1dXRQ45Y/8EN/ZdSqK1SGP/
|
||||
YQcR2Qkvny6qCeCt+yM8zpIWy6KiQcjm58h8aLOis3nK9rmDDSNmeQgl+k1OmJj5
|
||||
nUvFbnUdQZuEbhS0R7t6zGq+WT+j9uCIt0DV3m2+qWw0uN8ObZpr16GOWBp8yo6+
|
||||
3LBOwQ5+9/nqwTdOLMbpsZlJd/KOQikV9izOQkmL6tC2dxPXRqKPNST5987kYY37
|
||||
H+0iIfIyvFCx5gTKpoUy2JxGvXZOlKdibaav9b4P5P663YIb4sTUVPHi1cyZPYkc
|
||||
pnoLSjkCgYEA+zAuQ5i5FTLry7f5iPjGNSdWSJ8Fb8RJ6dqTBP/PDezI6K48vJJ1
|
||||
gkll3JMyXfMBScd90qq1QVAUmb8Nz2mQXCIHsNBexjeuGNBnXxB21XSM3bA+1mL9
|
||||
zJ5eUnBUgih60CJE/8jdo9GNUY0Ce0XEOTpl1LKYPJc2uSXC1y6kLcMCgYEA95C0
|
||||
6c9hat9/VDRab+0VZrMb+FBj3f1XsHGRSWbQDuWuO7CfOEYhwTcLrE9WexTEjNL6
|
||||
6IVvN3CqhcxGTHD4fgW+9QFGAXC2SoNKCsPokzC1X1ikUusPtriyn/IvUP6qYHus
|
||||
eB4xsCD5ulyGfJJRbBRa5dmzlqxnKyimPfC+MGkCgYEAxoibWHQifX3k3vyHb1pp
|
||||
luODkByYOHGllf9bSo1BwxjO5xGoEceUtyh6KS/ylE0YTI8vhM3GO1wnHCnkqXYf
|
||||
UqLW/0qCThr+MMCvo3So6CeZmzLNR7ewMAVQOcptEP8bqtwbOyww+mULVFSmjHZl
|
||||
FHJyv/101BcUepw89sT3oO8CgYB4GQI63vkCcLQDdHZfD+Ou87rg5pbcDVfp5940
|
||||
fqT2ZSP2HwPOt+8OHZcTG1X31aZYLs272WePvJ9s0yFTWgailEUD9H8ymaxFT5Wu
|
||||
zUVZimqie40UEKaJ3OYCw+mCYFjk/3o2t2cha43ag6JWcmD/joxeLxN5R9+wx0KG
|
||||
j/Cj6QKBgF0WuXRkQwItF2F/swibuFcnqlGjz6I5k5wBuuZiRk8PcPgCPSyoWald
|
||||
YzN6CLKfQSIZjO0fj7S329hc+CbKx1o+HNPjC72lP9fC/KCh8FAxMhUkcCwP7Bvt
|
||||
fpRG66KTIz6EZtWWAI/VjWxUo9LOX4YVTBSVLeMI+bNmKHw4VLKI
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB2DCCAX+gAwIBAgIUQni3EN5sA2Q87Jk7GtqIb6pm5vowCgYIKoZIzj0EAwIw
|
||||
QDEjMCEGA1UEAwwaaG9zdC5kb2NrZXIuaW50ZXJuYWwgc3ViQ0ExDDAKBgNVBAoM
|
||||
A1M0NDELMAkGA1UEBhMCVVMwHhcNMjUwOTE1MTkyMDAwWhcNMjYwOTE1MTkyMDAw
|
||||
WjA6MR0wGwYDVQQDDBRob3N0LmRvY2tlci5pbnRlcm5hbDEMMAoGA1UECgwDUzQ0
|
||||
MQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABMbfvWXNwXIL
|
||||
FEQlv3WKdyyu44SIjpBu7gh8ZJLhiwPh73ZeCdTYdVuMRK/MjXYdSarbFcyYx3r3
|
||||
tuA2b/ETwtOjXTBbMAkGA1UdEwQCMAAwDgYDVR0PAQH/BAQDAgWgMB0GA1UdDgQW
|
||||
BBTXX4f/xHDVTWWmoaxDhH5yJ7g31TAfBgNVHSMEGDAWgBSyPWWAFNKPvUUqo4Zv
|
||||
EsaDP9FhijAKBggqhkjOPQQDAgNHADBEAiBG2Px9xGFh99S3e8+LAF5oLxOR/+Kd
|
||||
pLmHIr0697N3wgIgSa5bHc9g3VYamhOlyW3ayqdvyiRKrXmZmNT9gxpInDA=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB5jCCAY2gAwIBAgIUUyG64WSxxu4aeQpcTtZVlG2I4MUwCgYIKoZIzj0EAwIw
|
||||
PzEiMCAGA1UEAwwZaG9zdC5kb2NrZXIuaW50ZXJuYWwgcm9vdDEMMAoGA1UECgwD
|
||||
UzQ0MQswCQYDVQQGEwJVUzAeFw0yNTA5MTUxOTIwMDBaFw0yNjA5MTUxOTIwMDBa
|
||||
MEAxIzAhBgNVBAMMGmhvc3QuZG9ja2VyLmludGVybmFsIHN1YkNBMQwwCgYDVQQK
|
||||
DANTNDQxCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEavrF
|
||||
mkwSpVhlLvFx23EcamEPzAZIVjOiqRoVKSfWRGnHRW7GClYHa89NINwwIAQZHRb+
|
||||
LnYGamI7OWCs2LKLkaNmMGQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E
|
||||
BAMCAYYwHQYDVR0OBBYEFLI9ZYAU0o+9RSqjhm8SxoM/0WGKMB8GA1UdIwQYMBaA
|
||||
FFCnxtBjFhTUJpJmL3scRJ3udfqMMAoGCCqGSM49BAMCA0cAMEQCIH3PQWW0To/T
|
||||
CqHBbRKXqMAaOH9Vu/+aX/Ka3dEt0H/rAiAxtOqJR6/XNNKGr7Ngss7m8bDquph9
|
||||
CcGBegQ+U2jW1w==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgV77VRtKNNXcJ/I+/
|
||||
iAfPlhPnl85Ba9fdZazc6wLNRtGhRANCAATG371lzcFyCxREJb91incsruOEiI6Q
|
||||
bu4IfGSS4YsD4e92XgnU2HVbjESvzI12HUmq2xXMmMd697bgNm/xE8LT
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,15 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICQjCCAemgAwIBAgIUZskYnMrPMTD8Y7xWa+OtNNtEZ0wwCgYIKoZIzj0EAwIw
|
||||
PzEiMCAGA1UEAwwZaG9zdC5kb2NrZXIuaW50ZXJuYWwgcm9vdDEMMAoGA1UECgwD
|
||||
UzQ0MQswCQYDVQQGEwJVUzAeFw0yNTA5MTUxOTIwMDBaFw0yNjA5MTUxOTIwMDBa
|
||||
MD8xIjAgBgNVBAMMGWhvc3QuZG9ja2VyLmludGVybmFsIHJvb3QxDDAKBgNVBAoM
|
||||
A1M0NDELMAkGA1UEBhMCVVMwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATC3k2b
|
||||
EozYv5BShaWRz1gkUV12qFlhT+fRYBHXxojziBTBgy+skq/Mg+QC9WvAGLvwAwMZ
|
||||
sekkQrqpOk7VhGvEo4HCMIG/MBIGA1UdEwEB/wQIMAYBAf8CAQEwDgYDVR0PAQH/
|
||||
BAQDAgGGMB0GA1UdDgQWBBRQp8bQYxYU1CaSZi97HESd7nX6jDB6BgNVHSMEczBx
|
||||
gBRQp8bQYxYU1CaSZi97HESd7nX6jKFDpEEwPzEiMCAGA1UEAwwZaG9zdC5kb2Nr
|
||||
ZXIuaW50ZXJuYWwgcm9vdDEMMAoGA1UECgwDUzQ0MQswCQYDVQQGEwJVU4IUZskY
|
||||
nMrPMTD8Y7xWa+OtNNtEZ0wwCgYIKoZIzj0EAwIDRwAwRAIgeWgobfOZaHGwW12R
|
||||
MhT+Nax9KPeTXYIsanAcOz/9hpECIF8bGUBgl1DBsUtQs8vjOcPbmEeauKXrLq06
|
||||
XoEnt+Ig
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgv50+aVWvqbqBIxIh
|
||||
UOu0IF0Gmyn+bycKwZSNqntDp3WhRANCAATC3k2bEozYv5BShaWRz1gkUV12qFlh
|
||||
T+fRYBHXxojziBTBgy+skq/Mg+QC9WvAGLvwAwMZsekkQrqpOk7VhGvE
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgdAgB+w3+nvkiW2Uk
|
||||
7bji/syJjVr8hJv1OfdMkKakL5ShRANCAARq+sWaTBKlWGUu8XHbcRxqYQ/MBkhW
|
||||
M6KpGhUpJ9ZEacdFbsYKVgdrz00g3DAgBBkdFv4udgZqYjs5YKzYsouR
|
||||
-----END PRIVATE KEY-----
|
||||
BIN
tools/citrineos-core-main/apps/Server/src/assets/logo.png
Normal file
BIN
tools/citrineos-core-main/apps/Server/src/assets/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,3 @@
|
||||
Copyright Contributors to the CitrineOS Project
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
839
tools/citrineos-core-main/apps/Server/src/citrineOSServer.ts
Normal file
839
tools/citrineos-core-main/apps/Server/src/citrineOSServer.ts
Normal file
@@ -0,0 +1,839 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type {
|
||||
AbstractModule,
|
||||
BootstrapConfig,
|
||||
IApiAuthProvider,
|
||||
IAuthorizer,
|
||||
ICache,
|
||||
IFileStorage,
|
||||
IMessageHandler,
|
||||
IMessageRouter,
|
||||
IModule,
|
||||
IModuleApi,
|
||||
SystemConfig,
|
||||
} from '@citrineos/base';
|
||||
import {
|
||||
Ajv,
|
||||
ConfigStoreFactory,
|
||||
EventGroup,
|
||||
eventGroupFromString,
|
||||
type IAuthenticator,
|
||||
OCPPValidator,
|
||||
OCPPVersion,
|
||||
} from '@citrineos/base';
|
||||
import type { ISmartCharging } from '@citrineos/core';
|
||||
import {
|
||||
AdminApi,
|
||||
apiAuthPluginFp,
|
||||
Authenticator,
|
||||
BasicAuthenticationFilter,
|
||||
BrokerAwareMessageSender,
|
||||
CertificateAuthorityService,
|
||||
CertificatesDataApi,
|
||||
CertificatesModule,
|
||||
CertificatesOcpp2Api,
|
||||
ConfigurationDataApi,
|
||||
ConfigurationModule,
|
||||
ConfigurationOcpp16Api,
|
||||
ConfigurationOcpp2Api,
|
||||
ConnectedStationFilter,
|
||||
DefaultDrizzleInstance,
|
||||
EVDriverDataApi,
|
||||
EVDriverModule,
|
||||
EVDriverOcpp16Api,
|
||||
EVDriverOcpp2Api,
|
||||
IdGenerator,
|
||||
initSwagger,
|
||||
InternalSmartCharging,
|
||||
LocalBypassAuthProvider,
|
||||
MemoryCache,
|
||||
MessageRouterImpl,
|
||||
MonitoringDataApi,
|
||||
MonitoringModule,
|
||||
MonitoringOcpp2Api,
|
||||
NetworkProfileFilter,
|
||||
OIDCAuthProvider,
|
||||
RabbitMQChannelManager,
|
||||
RabbitMQConnectionManager,
|
||||
RabbitMqReceiver,
|
||||
RabbitMqSender,
|
||||
RealTimeAuthorizer,
|
||||
RedisCache,
|
||||
ReportingModule,
|
||||
ReportingOcpp16Api,
|
||||
ReportingOcpp2Api,
|
||||
RepositoryStore,
|
||||
sequelize,
|
||||
Sequelize,
|
||||
SmartChargingModule,
|
||||
SmartChargingOcpp16Api,
|
||||
SmartChargingOcpp2Api,
|
||||
TenantDataApi,
|
||||
TenantModule,
|
||||
TransactionsDataApi,
|
||||
TransactionsModule,
|
||||
TransactionsOcpp2Api,
|
||||
UnknownStationFilter,
|
||||
WebhookDispatcher,
|
||||
WebsocketNetworkConnection,
|
||||
} from '@citrineos/core';
|
||||
import cors from '@fastify/cors';
|
||||
import { type JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts';
|
||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
||||
import fastify from 'fastify';
|
||||
import type {
|
||||
FastifyRouteSchemaDef,
|
||||
FastifySchemaCompiler,
|
||||
FastifyValidationResult,
|
||||
} from 'fastify/types/schema.js';
|
||||
import type { RedisClientOptions } from 'redis';
|
||||
import { type ILogObj, Logger } from 'tslog';
|
||||
import { type HealthCheckResult, HealthCheckService } from './health/HealthCheckService.js';
|
||||
|
||||
export class CitrineOSServer {
|
||||
/**
|
||||
* Fields
|
||||
*/
|
||||
protected readonly _config: BootstrapConfig & SystemConfig;
|
||||
protected readonly _logger: Logger<ILogObj>;
|
||||
protected readonly _server: FastifyInstance;
|
||||
protected readonly _cache: ICache;
|
||||
protected readonly _ajv: Ajv.Ajv;
|
||||
protected readonly _ocppValidator: OCPPValidator;
|
||||
protected readonly _fileStorage: IFileStorage;
|
||||
protected readonly modules: IModule[] = [];
|
||||
protected readonly apis: IModuleApi[] = [];
|
||||
protected _sequelizeInstance!: Sequelize;
|
||||
protected host?: string;
|
||||
protected port?: number;
|
||||
protected eventGroup?: EventGroup;
|
||||
protected _authenticator?: IAuthenticator;
|
||||
protected _router?: IMessageRouter;
|
||||
protected _networkConnection?: WebsocketNetworkConnection;
|
||||
protected _repositoryStore!: RepositoryStore;
|
||||
protected _idGenerator!: IdGenerator;
|
||||
protected _certificateAuthorityService!: CertificateAuthorityService;
|
||||
protected _smartChargingService!: ISmartCharging;
|
||||
protected _realTimeAuthorizer!: IAuthorizer;
|
||||
|
||||
protected readonly appName: string;
|
||||
protected _isShuttingDown = false;
|
||||
protected _connectionManager?: RabbitMQConnectionManager;
|
||||
protected _channelManager?: RabbitMQChannelManager;
|
||||
protected _healthCheckService?: HealthCheckService;
|
||||
|
||||
/**
|
||||
* Constructor for the class.
|
||||
*
|
||||
* @param {EventGroup} appName - app type
|
||||
* @param {BootstrapConfig} bootstrapConfig
|
||||
* @param {SystemConfig} systemConfig - config
|
||||
* @param {FastifyInstance} server - optional Fastify server instance
|
||||
* @param {Ajv} ajv - optional Ajv JSON schema validator instance
|
||||
* @param {ICache} cache - cache
|
||||
* @param {IFileStorage} _fileStorage - file storage
|
||||
*/
|
||||
// todo rename event group to type
|
||||
constructor(
|
||||
appName: string,
|
||||
bootstrapConfig: BootstrapConfig,
|
||||
systemConfig: SystemConfig,
|
||||
server?: FastifyInstance,
|
||||
ajv?: Ajv.Ajv,
|
||||
cache?: ICache,
|
||||
_fileStorage?: IFileStorage,
|
||||
) {
|
||||
// TODO: Create and export config schemas for each util module, such as amqp, redis, etc, to avoid passing them possibly invalid configuration
|
||||
if (!systemConfig.util.messageBroker.amqp) {
|
||||
throw new Error('This server implementation requires amqp configuration for rabbitMQ.');
|
||||
}
|
||||
|
||||
this.appName = appName;
|
||||
this._config = { ...bootstrapConfig, ...systemConfig };
|
||||
this._server = server || fastify().withTypeProvider<JsonSchemaToTsProvider>();
|
||||
|
||||
// enable cors
|
||||
(this._server as any).register(cors, {
|
||||
origin: true, // This can be customized to specify allowed origins
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // Specify allowed HTTP methods
|
||||
});
|
||||
|
||||
console.log('Bootstrap configuration loaded');
|
||||
// Add health check
|
||||
this.initHealthCheck();
|
||||
|
||||
// Create Ajv JSON schema validator instance
|
||||
this._ajv = OCPPValidator.createServerAjvInstance(ajv);
|
||||
|
||||
// Initialize parent logger
|
||||
this._logger = this.initLogger();
|
||||
|
||||
// Create a separate OCPPValidator with its own Ajv instance for OCPP message validation.
|
||||
// This must be distinct from _ajv: OCPP messages are parsed JSON (no coercion needed),
|
||||
// whereas _ajv coerces types for Fastify's HTTP schema compilation.
|
||||
this._ocppValidator = new OCPPValidator(this._logger);
|
||||
|
||||
// Set cache implementation
|
||||
this._cache = this.initCache(cache);
|
||||
|
||||
// Initialize Swagger if enabled
|
||||
this.initSwagger()
|
||||
.then()
|
||||
.catch((error) => this._logger.error('Could not initialize swagger', { error }));
|
||||
|
||||
// Register API authentication
|
||||
this.registerApiAuth();
|
||||
|
||||
// Initialize File Access Implementation
|
||||
this._fileStorage = ConfigStoreFactory.getInstance();
|
||||
|
||||
// Register AJV for schema validation
|
||||
this.registerAjv();
|
||||
|
||||
// Initialize repository store
|
||||
this.initRepositoryStore();
|
||||
this.initIdGenerator();
|
||||
this.initCertificateAuthorityService();
|
||||
this.initSmartChargingService();
|
||||
this.initRealTimeAuthorizer();
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
await this.initMessageBrokerConnection();
|
||||
// Initialize module & API
|
||||
// Always initialize API after SwaggerUI
|
||||
await this.initSystem();
|
||||
// Initialize database
|
||||
await this.initDb();
|
||||
|
||||
this.initHealthCheckService();
|
||||
|
||||
// Set up shutdown handlers
|
||||
for (const event of ['SIGINT', 'SIGTERM', 'SIGQUIT']) {
|
||||
process.on(event, () => {
|
||||
this._logger.info(`Received ${event}`);
|
||||
this.shutdown().catch((err) => {
|
||||
console.error('Shutdown error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
async shutdown() {
|
||||
if (this._isShuttingDown) return;
|
||||
this._isShuttingDown = true;
|
||||
this._logger.info('Shutdown initiated');
|
||||
this._healthCheckService?.shutdown();
|
||||
|
||||
const forceExit = setTimeout(() => {
|
||||
console.log('Shutdown timed out, forcing exit');
|
||||
process.exit(1);
|
||||
}, this._config.shutdownGracePeriodSeconds * 1000); // Default is 30 seconds
|
||||
forceExit.unref();
|
||||
|
||||
this._logger.info('Closing HTTP server...');
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
this._server.close(() => resolve());
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
this._logger.info('Closing WebSocket servers...');
|
||||
await this._networkConnection?.shutdown();
|
||||
|
||||
this._logger.info('Closing RabbitMQ connections...');
|
||||
await this._channelManager?.closeAll();
|
||||
await this._connectionManager?.close();
|
||||
|
||||
this._logger.info('Closing PostgreSQL connections...');
|
||||
await this._sequelizeInstance.connectionManager.close();
|
||||
|
||||
this._logger.info('Shutdown complete');
|
||||
process.exitCode = 0;
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
await this.initialize();
|
||||
await this._syncWebsocketConfig();
|
||||
await this._server
|
||||
.listen({
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
})
|
||||
.then((address) => {
|
||||
this._logger?.info(`Server listening at ${address}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
this._logger?.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
// TODO Push config to microservices
|
||||
} catch (error) {
|
||||
await Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
protected async _syncWebsocketConfig() {
|
||||
for (const websocketServerConfig of this._config.util.networkConnection.websocketServers) {
|
||||
await this._repositoryStore.serverNetworkProfileRepository.upsertServerNetworkProfile(
|
||||
websocketServerConfig,
|
||||
this._config.maxCallLengthSeconds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async initMessageBrokerConnection(): Promise<any> {
|
||||
const url = this._config.util.messageBroker.amqp?.url;
|
||||
if (!url) {
|
||||
throw new Error('RabbitMQ URL is not configured');
|
||||
}
|
||||
this._connectionManager = new RabbitMQConnectionManager(this._config.maxReconnectDelay, url);
|
||||
this._channelManager = new RabbitMQChannelManager(this._connectionManager);
|
||||
await this._connectionManager.connect();
|
||||
}
|
||||
|
||||
protected _createSender(): BrokerAwareMessageSender {
|
||||
const exchange = this._config.util.messageBroker.amqp?.exchange;
|
||||
if (!exchange) {
|
||||
throw new Error('RabbitMQ exchange is not configured');
|
||||
}
|
||||
if (!this._connectionManager || !this._channelManager) {
|
||||
throw new Error('RabbitMQ connection or channel manager is not initialized');
|
||||
}
|
||||
return new BrokerAwareMessageSender(
|
||||
new RabbitMqSender(exchange, this._connectionManager, this._channelManager, this._logger),
|
||||
this._connectionManager,
|
||||
this._config.maxCallLengthSeconds,
|
||||
this._logger,
|
||||
);
|
||||
}
|
||||
|
||||
protected _createHandler(): IMessageHandler {
|
||||
return new RabbitMqReceiver(this._config, this._channelManager!, this._logger);
|
||||
}
|
||||
|
||||
protected initHealthCheck() {
|
||||
const respond = (reply: FastifyReply, result: HealthCheckResult) =>
|
||||
reply
|
||||
.code(result.status === 'pass' ? 200 : 503)
|
||||
.header('Content-Type', 'application/health+json')
|
||||
.send(result);
|
||||
|
||||
const liveness = async (_req: any, reply: FastifyReply) =>
|
||||
respond(
|
||||
reply,
|
||||
this._healthCheckService
|
||||
? this._healthCheckService.checkLiveness()
|
||||
: { status: 'pass', checks: {} },
|
||||
);
|
||||
|
||||
const readiness = async (_req: any, reply: FastifyReply) => {
|
||||
if (!this._healthCheckService) {
|
||||
return respond(reply, {
|
||||
status: 'fail',
|
||||
checks: { init: { status: 'fail', error: 'not yet initialized' } },
|
||||
});
|
||||
}
|
||||
return respond(reply, await this._healthCheckService.checkReadiness());
|
||||
};
|
||||
|
||||
this._server.get('/health', liveness);
|
||||
this._server.get('/health/live', liveness);
|
||||
this._server.get('/health/ready', readiness);
|
||||
}
|
||||
|
||||
protected initHealthCheckService() {
|
||||
this._healthCheckService = new HealthCheckService(
|
||||
this._networkConnection,
|
||||
this._connectionManager,
|
||||
this._cache,
|
||||
this._sequelizeInstance,
|
||||
this._config.notReadyThresholdSeconds,
|
||||
this._logger,
|
||||
);
|
||||
}
|
||||
|
||||
protected initLogger() {
|
||||
const isCloud = process.env.DEPLOYMENT_TARGET === 'cloud';
|
||||
|
||||
const loggerSettings = {
|
||||
name: 'CitrineOS Logger',
|
||||
minLevel: this._config.logLevel,
|
||||
hideLogPositionForProduction: this._config.env === 'production',
|
||||
type: isCloud ? ('json' as const) : ('pretty' as const),
|
||||
};
|
||||
|
||||
return new Logger<ILogObj>(loggerSettings);
|
||||
}
|
||||
|
||||
protected async initDb() {
|
||||
await sequelize.DefaultSequelizeInstance.initializeSequelize();
|
||||
if (process.env.CITRINEOS_USE_DRIZZLE_SECURITY_EVENT === 'true') {
|
||||
await DefaultDrizzleInstance.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
protected initCache(cache?: ICache): ICache {
|
||||
if (cache) return cache;
|
||||
if (this._config.util.cache.redis) {
|
||||
const redisClientOptions: RedisClientOptions =
|
||||
'url' in this._config.util.cache.redis
|
||||
? { url: this._config.util.cache.redis.url }
|
||||
: {
|
||||
socket: {
|
||||
host: this._config.util.cache.redis.host,
|
||||
port: this._config.util.cache.redis.port,
|
||||
},
|
||||
};
|
||||
return new RedisCache(redisClientOptions, this._logger);
|
||||
}
|
||||
return new MemoryCache();
|
||||
}
|
||||
|
||||
protected async initSwagger() {
|
||||
if (this._config.util.swagger) {
|
||||
await initSwagger(this._config, this._server);
|
||||
}
|
||||
}
|
||||
|
||||
protected registerAjv() {
|
||||
// todo type schema instead of any
|
||||
const fastifySchemaCompiler: FastifySchemaCompiler<any> = (
|
||||
routeSchema: FastifyRouteSchemaDef<any>,
|
||||
) => this._ajv?.compile(routeSchema.schema) as FastifyValidationResult;
|
||||
this._server.setValidatorCompiler(fastifySchemaCompiler);
|
||||
}
|
||||
|
||||
protected registerApiAuth() {
|
||||
const authProvider = this.initApiAuthProvider();
|
||||
this._server.register(apiAuthPluginFp, {
|
||||
provider: authProvider,
|
||||
options: {
|
||||
excludedRoutes: [
|
||||
'/health',
|
||||
'/health/live',
|
||||
'/health/ready',
|
||||
'/docs', // API documentation
|
||||
],
|
||||
debug: this._config.logLevel <= 2, // Enable debug logs in dev mode
|
||||
},
|
||||
logger: this._logger,
|
||||
});
|
||||
}
|
||||
|
||||
protected initNetworkConnection() {
|
||||
this._authenticator = new Authenticator(
|
||||
new UnknownStationFilter(
|
||||
new sequelize.SequelizeLocationRepository(this._config, this._logger),
|
||||
this._logger,
|
||||
),
|
||||
new ConnectedStationFilter(this._cache, this._logger),
|
||||
new NetworkProfileFilter(
|
||||
new sequelize.SequelizeDeviceModelRepository(this._config, this._logger),
|
||||
this._logger,
|
||||
),
|
||||
new BasicAuthenticationFilter(
|
||||
new sequelize.SequelizeDeviceModelRepository(this._config, this._logger),
|
||||
this._logger,
|
||||
),
|
||||
this._logger,
|
||||
);
|
||||
|
||||
const webhookDispatcher = new WebhookDispatcher(
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
this._repositoryStore.subscriptionRepository,
|
||||
this._logger,
|
||||
);
|
||||
|
||||
const routerSender = this._createSender();
|
||||
|
||||
this._router = new MessageRouterImpl(
|
||||
this._config,
|
||||
this._cache,
|
||||
routerSender,
|
||||
this._createHandler(),
|
||||
webhookDispatcher,
|
||||
async (_identifier: string, _message: string) => {},
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.locationRepository,
|
||||
);
|
||||
|
||||
this._networkConnection = new WebsocketNetworkConnection(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._authenticator,
|
||||
this._router,
|
||||
this._fileStorage,
|
||||
this._logger,
|
||||
this._repositoryStore.locationRepository.doesChargingStationExistByStationId.bind(
|
||||
this._repositoryStore.locationRepository,
|
||||
),
|
||||
async (tenantId: number) => {
|
||||
const tenant = await this._repositoryStore.tenantRepository.readByKey(tenantId, tenantId);
|
||||
return tenant?.maxChargingStations ?? null;
|
||||
},
|
||||
this._connectionManager,
|
||||
);
|
||||
|
||||
routerSender.onCallTimeout = (ocppConnectionName, tenantId) =>
|
||||
this._networkConnection!.disconnect(tenantId, ocppConnectionName).then(() => undefined);
|
||||
|
||||
this._router.networkHook = this._networkConnection.bindNetworkHook();
|
||||
|
||||
this.apis.push(
|
||||
new AdminApi(
|
||||
this._router,
|
||||
this._networkConnection,
|
||||
this._server,
|
||||
this._config,
|
||||
this._logger,
|
||||
this._repositoryStore.subscriptionRepository,
|
||||
this._repositoryStore.serverNetworkProfileRepository,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initHandlersAndAddModule(module: AbstractModule) {
|
||||
await module.initHandlers();
|
||||
this.modules.push(module);
|
||||
}
|
||||
|
||||
protected async initAllModules() {
|
||||
if (this._config.modules.certificates) {
|
||||
await this.initCertificatesModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.configuration) {
|
||||
await this.initConfigurationModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.evdriver) {
|
||||
await this.initEVDriverModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.monitoring) {
|
||||
await this.initMonitoringModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.reporting) {
|
||||
await this.initReportingModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.smartcharging) {
|
||||
await this.initSmartChargingModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.transactions) {
|
||||
await this.initTransactionsModule();
|
||||
}
|
||||
|
||||
if (this._config.modules.tenant) {
|
||||
await this.initTenantModule();
|
||||
}
|
||||
}
|
||||
|
||||
protected initApiAuthProvider(): IApiAuthProvider {
|
||||
this._logger.info('Initializing API authentication provider,', this._config.util.authProvider);
|
||||
if (this._config.util.authProvider.oidc) {
|
||||
return new OIDCAuthProvider(this._config.util.authProvider.oidc, this._logger);
|
||||
} else if (this._config.util.authProvider.localByPass) {
|
||||
return new LocalBypassAuthProvider(this._logger);
|
||||
} else {
|
||||
throw new Error('No valid API authentication provider configured');
|
||||
}
|
||||
}
|
||||
|
||||
protected async initCertificatesModule() {
|
||||
const module = new CertificatesModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._fileStorage,
|
||||
this._networkConnection!,
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.certificateRepository,
|
||||
this._repositoryStore.installedCertificateRepository,
|
||||
this._repositoryStore.installCertificateAttemptRepository,
|
||||
this._repositoryStore.deleteCertificateAttemptRepository,
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new CertificatesOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new CertificatesOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new CertificatesDataApi(
|
||||
module,
|
||||
this._server,
|
||||
this._fileStorage,
|
||||
this._config.util.networkConnection.websocketServers,
|
||||
this._logger,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initConfigurationModule() {
|
||||
const module = new ConfigurationModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.bootRepository,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.messageInfoRepository,
|
||||
this._repositoryStore.locationRepository,
|
||||
this._repositoryStore.changeConfigurationRepository,
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
this._idGenerator,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new ConfigurationOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new ConfigurationOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new ConfigurationOcpp16Api(module, this._server, this._logger),
|
||||
new ConfigurationDataApi(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initEVDriverModule() {
|
||||
const module = new EVDriverModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.authorizationRepository,
|
||||
this._repositoryStore.localAuthListRepository,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.tariffRepository,
|
||||
this._repositoryStore.transactionEventRepository,
|
||||
this._repositoryStore.chargingProfileRepository,
|
||||
this._repositoryStore.reservationRepository,
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
this._repositoryStore.locationRepository,
|
||||
this._certificateAuthorityService,
|
||||
this._realTimeAuthorizer,
|
||||
[],
|
||||
this._idGenerator,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new EVDriverOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new EVDriverOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new EVDriverOcpp16Api(module, this._server, this._logger),
|
||||
new EVDriverDataApi(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initMonitoringModule() {
|
||||
const module = new MonitoringModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.variableMonitoringRepository,
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
this._idGenerator,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new MonitoringOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new MonitoringOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new MonitoringDataApi(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initReportingModule() {
|
||||
const module = new ReportingModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.securityEventRepository,
|
||||
this._repositoryStore.variableMonitoringRepository,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new ReportingOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new ReportingOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new ReportingOcpp16Api(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initSmartChargingModule() {
|
||||
const module = new SmartChargingModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.transactionEventRepository,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.chargingProfileRepository,
|
||||
this._smartChargingService,
|
||||
this._idGenerator,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new SmartChargingOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new SmartChargingOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new SmartChargingOcpp16Api(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initTransactionsModule() {
|
||||
const module = new TransactionsModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._fileStorage,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.transactionEventRepository,
|
||||
this._repositoryStore.authorizationRepository,
|
||||
this._repositoryStore.deviceModelRepository,
|
||||
this._repositoryStore.componentRepository,
|
||||
this._repositoryStore.locationRepository,
|
||||
this._repositoryStore.tariffRepository,
|
||||
this._repositoryStore.reservationRepository,
|
||||
this._repositoryStore.ocppMessageRepository,
|
||||
this._realTimeAuthorizer,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(
|
||||
new TransactionsOcpp2Api(module, this._server, OCPPVersion.OCPP2_0_1, this._logger),
|
||||
new TransactionsOcpp2Api(module, this._server, OCPPVersion.OCPP2_1, this._logger),
|
||||
new TransactionsDataApi(module, this._server, this._logger),
|
||||
);
|
||||
}
|
||||
|
||||
protected async initTenantModule() {
|
||||
const module = new TenantModule(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._createSender(),
|
||||
this._createHandler(),
|
||||
this._logger,
|
||||
this._ocppValidator,
|
||||
this._repositoryStore.tenantRepository,
|
||||
);
|
||||
await this.initHandlersAndAddModule(module);
|
||||
this.apis.push(new TenantDataApi(module, this._server, this._logger));
|
||||
this._logger.info('Tenant module initialized');
|
||||
}
|
||||
|
||||
protected async initModule(eventGroup = this.eventGroup) {
|
||||
this._logger.info(`Initializing module: ${this.appName}`);
|
||||
switch (eventGroup) {
|
||||
case EventGroup.Certificates:
|
||||
await this.initCertificatesModule();
|
||||
break;
|
||||
case EventGroup.Configuration:
|
||||
await this.initConfigurationModule();
|
||||
break;
|
||||
case EventGroup.EVDriver:
|
||||
await this.initEVDriverModule();
|
||||
break;
|
||||
case EventGroup.Monitoring:
|
||||
await this.initMonitoringModule();
|
||||
break;
|
||||
case EventGroup.Reporting:
|
||||
await this.initReportingModule();
|
||||
break;
|
||||
case EventGroup.SmartCharging:
|
||||
await this.initSmartChargingModule();
|
||||
break;
|
||||
case EventGroup.Transactions:
|
||||
await this.initTransactionsModule();
|
||||
break;
|
||||
case EventGroup.Tenant:
|
||||
await this.initTenantModule();
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unhandled module type: ' + this.appName);
|
||||
}
|
||||
}
|
||||
|
||||
protected async initSystem() {
|
||||
this.eventGroup = eventGroupFromString(this.appName);
|
||||
|
||||
this.host = this._config.centralSystem.host;
|
||||
this.port = this._config.centralSystem.port;
|
||||
|
||||
if (this.eventGroup === EventGroup.All) {
|
||||
this._logger.info('Initializing in ALL mode: WebSocket server and all modules');
|
||||
this.initNetworkConnection();
|
||||
await this.initAllModules();
|
||||
} else if (this.eventGroup === EventGroup.Router) {
|
||||
this._logger.info('Initializing in ROUTER mode: WebSocket server, no modules');
|
||||
// OCPP Router only: WebSocket server, no modules
|
||||
this.initNetworkConnection();
|
||||
} else if (this.eventGroup === EventGroup.Modules) {
|
||||
// All modules, no WebSocket server
|
||||
this._logger.info('Initializing in MODULES mode: all modules without NetworkConnection');
|
||||
await this.initAllModules();
|
||||
} else {
|
||||
await this.initModule();
|
||||
}
|
||||
}
|
||||
|
||||
protected initRepositoryStore() {
|
||||
this._sequelizeInstance = sequelize.DefaultSequelizeInstance.getInstance(
|
||||
this._config,
|
||||
this._logger,
|
||||
);
|
||||
this._repositoryStore = new RepositoryStore(
|
||||
this._config,
|
||||
this._logger,
|
||||
this._sequelizeInstance,
|
||||
);
|
||||
}
|
||||
|
||||
protected initIdGenerator() {
|
||||
this._idGenerator = new IdGenerator(this._repositoryStore.chargingStationSequenceRepository);
|
||||
}
|
||||
|
||||
protected initCertificateAuthorityService() {
|
||||
this._certificateAuthorityService = new CertificateAuthorityService(
|
||||
this._config,
|
||||
this._cache,
|
||||
this._logger,
|
||||
undefined,
|
||||
undefined,
|
||||
this._fileStorage,
|
||||
);
|
||||
}
|
||||
|
||||
protected initSmartChargingService() {
|
||||
this._smartChargingService = new InternalSmartCharging(
|
||||
this._repositoryStore.chargingProfileRepository,
|
||||
);
|
||||
}
|
||||
|
||||
protected initRealTimeAuthorizer() {
|
||||
this._realTimeAuthorizer = new RealTimeAuthorizer(
|
||||
this._repositoryStore.locationRepository,
|
||||
this._config,
|
||||
this._logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import type { BootstrapConfig, ConfigStore, SystemConfig } from '@citrineos/base';
|
||||
import { ConfigStoreFactory, defineConfig } from '@citrineos/base';
|
||||
import { GcpCloudStorage, LocalStorage, S3Storage } from '@citrineos/core';
|
||||
|
||||
/**
|
||||
* Helper function to create the appropriate ConfigStore based on bootstrap config
|
||||
*/
|
||||
function createConfigStore(bootstrapConfig: BootstrapConfig): ConfigStore {
|
||||
switch (bootstrapConfig.fileAccess.type) {
|
||||
case 'local':
|
||||
return new LocalStorage(
|
||||
bootstrapConfig.fileAccess.local!.defaultFilePath,
|
||||
bootstrapConfig.configFileName,
|
||||
bootstrapConfig.configDir,
|
||||
);
|
||||
case 's3':
|
||||
return new S3Storage(
|
||||
bootstrapConfig.fileAccess.s3!,
|
||||
bootstrapConfig.configFileName,
|
||||
bootstrapConfig.configDir,
|
||||
);
|
||||
case 'gcp':
|
||||
return new GcpCloudStorage(
|
||||
bootstrapConfig.fileAccess.gcp!,
|
||||
bootstrapConfig.configFileName,
|
||||
bootstrapConfig.configDir!,
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unsupported file access type: ${bootstrapConfig.fileAccess.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the system configuration
|
||||
* 1. Loads bootstrap config from environment variables
|
||||
* 2. Uses bootstrap config to create a ConfigStore
|
||||
* 3. Loads full config from storage or creates default if none exists
|
||||
* 4. Applies environment variable overrides for secrets and other values
|
||||
* 5. Validates the final config
|
||||
* @param defaultConfig Optional default config to use if no config exists in storage
|
||||
* @returns Promise resolving to the validated SystemConfig
|
||||
*/
|
||||
export async function loadSystemConfig(
|
||||
bootstrapConfig: BootstrapConfig,
|
||||
defaultConfig?: SystemConfig,
|
||||
): Promise<SystemConfig> {
|
||||
try {
|
||||
const configStore = createConfigStore(bootstrapConfig);
|
||||
ConfigStoreFactory.setConfigStore(configStore);
|
||||
console.log('Config store initialized');
|
||||
|
||||
let config: SystemConfig | null =
|
||||
process.env.CONFIG_CITRINEOS_WIPE_FILE_ON_START?.toLowerCase() === 'true'
|
||||
? null
|
||||
: await configStore.fetchConfig();
|
||||
|
||||
if (!config) {
|
||||
if (!defaultConfig) {
|
||||
throw new Error('No configuration found in storage and no default config provided');
|
||||
}
|
||||
|
||||
console.warn('No config found in storage. Creating default config...');
|
||||
config = defaultConfig;
|
||||
await configStore.saveConfig(config);
|
||||
console.log('Default config saved to storage');
|
||||
} else {
|
||||
console.log('Configuration loaded from storage');
|
||||
}
|
||||
|
||||
const validatedConfig = defineConfig(config);
|
||||
|
||||
return validatedConfig;
|
||||
} catch (error) {
|
||||
console.error('Failed to load system configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
304
tools/citrineos-core-main/apps/Server/src/config/envs/docker.ts
Executable file
304
tools/citrineos-core-main/apps/Server/src/config/envs/docker.ts
Executable file
@@ -0,0 +1,304 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
DEFAULT_TENANT_ID,
|
||||
defineConfig,
|
||||
HUBJECT_DEFAULT_BASEURL,
|
||||
HUBJECT_DEFAULT_CLIENTID,
|
||||
HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
HUBJECT_DEFAULT_TOKENURL,
|
||||
OCPP1_6,
|
||||
OCPP2_0_1,
|
||||
OCPP_CallAction,
|
||||
OCPP_VERSION_LIST,
|
||||
} from '@citrineos/base';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
export function createDockerConfig() {
|
||||
return defineConfig({
|
||||
env: 'development',
|
||||
centralSystem: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
},
|
||||
modules: {
|
||||
certificates: {
|
||||
endpointPrefix: '/certificates',
|
||||
responses: [
|
||||
OCPP_CallAction.CertificateSigned,
|
||||
OCPP_CallAction.DeleteCertificate,
|
||||
OCPP_CallAction.GetInstalledCertificateIds,
|
||||
OCPP_CallAction.InstallCertificate,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.Get15118EVCertificate,
|
||||
OCPP_CallAction.GetCertificateStatus,
|
||||
OCPP_CallAction.SignCertificate,
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
responses: [
|
||||
OCPP_CallAction.ChangeAvailability,
|
||||
OCPP_CallAction.ClearDisplayMessage,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.GetDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmware,
|
||||
OCPP_CallAction.Reset,
|
||||
OCPP_CallAction.SetDisplayMessage,
|
||||
OCPP_CallAction.SetNetworkProfile,
|
||||
OCPP_CallAction.TriggerMessage,
|
||||
OCPP_CallAction.UnpublishFirmware,
|
||||
OCPP_CallAction.UpdateFirmware,
|
||||
OCPP_CallAction.ChangeConfiguration,
|
||||
OCPP_CallAction.GetConfiguration,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.BootNotification,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.FirmwareStatusNotification,
|
||||
OCPP_CallAction.Heartbeat,
|
||||
OCPP_CallAction.NotifyDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmwareStatusNotification,
|
||||
],
|
||||
heartbeatInterval: 60,
|
||||
bootRetryInterval: 15,
|
||||
ocpp2_0_1: {
|
||||
unknownChargerStatus: OCPP2_0_1.RegistrationStatusEnumType.Accepted,
|
||||
getBaseReportOnPending: true,
|
||||
bootWithRejectedVariables: true,
|
||||
autoAccept: true,
|
||||
},
|
||||
ocpp1_6: {
|
||||
unknownChargerStatus: OCPP1_6.BootNotificationResponseStatus.Accepted,
|
||||
},
|
||||
endpointPrefix: '/configuration',
|
||||
},
|
||||
evdriver: {
|
||||
endpointPrefix: '/evdriver',
|
||||
enableGetChargingProfilesOnStartTransaction: true,
|
||||
responses: [
|
||||
OCPP_CallAction.CancelReservation,
|
||||
OCPP_CallAction.ClearCache,
|
||||
OCPP_CallAction.GetLocalListVersion,
|
||||
OCPP_CallAction.RequestStartTransaction,
|
||||
OCPP_CallAction.RequestStopTransaction,
|
||||
OCPP_CallAction.ReserveNow,
|
||||
OCPP_CallAction.SendLocalList,
|
||||
OCPP_CallAction.UnlockConnector,
|
||||
OCPP_CallAction.RemoteStopTransaction,
|
||||
OCPP_CallAction.RemoteStartTransaction,
|
||||
OCPP_CallAction.NotifyWebPaymentStarted,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.Authorize,
|
||||
OCPP_CallAction.ReservationStatusUpdate,
|
||||
OCPP_CallAction.VatNumberValidation,
|
||||
],
|
||||
},
|
||||
monitoring: {
|
||||
endpointPrefix: '/monitoring',
|
||||
responses: [
|
||||
OCPP_CallAction.ClearVariableMonitoring,
|
||||
OCPP_CallAction.GetVariables,
|
||||
OCPP_CallAction.SetMonitoringBase,
|
||||
OCPP_CallAction.SetMonitoringLevel,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
OCPP_CallAction.SetVariableMonitoring,
|
||||
OCPP_CallAction.SetVariables,
|
||||
],
|
||||
requests: [OCPP_CallAction.NotifyEvent],
|
||||
},
|
||||
reporting: {
|
||||
endpointPrefix: '/reporting',
|
||||
responses: [
|
||||
OCPP_CallAction.CustomerInformation,
|
||||
OCPP_CallAction.GetLog,
|
||||
OCPP_CallAction.GetReport,
|
||||
OCPP_CallAction.GetBaseReport,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.LogStatusNotification,
|
||||
OCPP_CallAction.NotifyCustomerInformation,
|
||||
OCPP_CallAction.NotifyReport,
|
||||
OCPP_CallAction.SecurityEventNotification,
|
||||
OCPP_CallAction.NotifyMonitoringReport,
|
||||
],
|
||||
},
|
||||
smartcharging: {
|
||||
endpointPrefix: '/smartcharging',
|
||||
responses: [
|
||||
OCPP_CallAction.ClearChargingProfile,
|
||||
OCPP_CallAction.GetChargingProfiles,
|
||||
OCPP_CallAction.GetCompositeSchedule,
|
||||
OCPP_CallAction.SetChargingProfile,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.ClearedChargingLimit,
|
||||
OCPP_CallAction.NotifyChargingLimit,
|
||||
OCPP_CallAction.NotifyEVChargingNeeds,
|
||||
OCPP_CallAction.NotifyEVChargingSchedule,
|
||||
OCPP_CallAction.ReportChargingProfiles,
|
||||
],
|
||||
},
|
||||
tenant: {
|
||||
endpointPrefix: '/tenant',
|
||||
responses: [],
|
||||
requests: [],
|
||||
},
|
||||
transactions: {
|
||||
endpointPrefix: '/transactions',
|
||||
costUpdatedInterval: 60,
|
||||
responses: [
|
||||
OCPP_CallAction.CostUpdated,
|
||||
OCPP_CallAction.GetTransactionStatus,
|
||||
OCPP_CallAction.SetDefaultTariff,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.MeterValues,
|
||||
OCPP_CallAction.StatusNotification,
|
||||
OCPP_CallAction.TransactionEvent,
|
||||
OCPP_CallAction.StatusNotification,
|
||||
OCPP_CallAction.StartTransaction,
|
||||
OCPP_CallAction.StopTransaction,
|
||||
OCPP_CallAction.NotifySettlement,
|
||||
],
|
||||
},
|
||||
},
|
||||
util: {
|
||||
cache: {
|
||||
memory: true,
|
||||
},
|
||||
messageBroker: {
|
||||
amqp: {
|
||||
url: 'amqp://guest:guest@amqp-broker:5672',
|
||||
exchange: 'citrineos',
|
||||
},
|
||||
},
|
||||
authProvider: {
|
||||
localByPass: true,
|
||||
},
|
||||
swagger: {
|
||||
path: '/docs',
|
||||
logoPath: path.resolve(path.dirname(__filename), '../../assets/logo.png'),
|
||||
exposeData: true,
|
||||
exposeMessage: true,
|
||||
},
|
||||
networkConnection: {
|
||||
websocketServers: [
|
||||
{
|
||||
id: '0',
|
||||
securityProfile: 0,
|
||||
allowUnknownChargingStations: true,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8081,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
//Uncomment to debug or use a specific port
|
||||
//forceProtocol: OCPPVersion.OCPP2_0_1
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
securityProfile: 1,
|
||||
allowUnknownChargingStations: false,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8082,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
securityProfile: 2,
|
||||
allowUnknownChargingStations: false,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8443,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tlsKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/leafKey.pem',
|
||||
),
|
||||
tlsCertificateChainFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/certChain.pem',
|
||||
),
|
||||
rootCACertificateFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/rootCertificate.pem',
|
||||
),
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
securityProfile: 3,
|
||||
allowUnknownChargingStations: false,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8444,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tlsKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/leafKey.pem',
|
||||
),
|
||||
tlsCertificateChainFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/certChain.pem',
|
||||
),
|
||||
mtlsCertificateAuthorityKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/subCAKey.pem',
|
||||
),
|
||||
rootCACertificateFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/rootCertificate.pem',
|
||||
),
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
certificateAuthority: {
|
||||
v2gCA: {
|
||||
name: 'hubject',
|
||||
hubject: {
|
||||
baseUrl: HUBJECT_DEFAULT_BASEURL,
|
||||
tokenUrl: HUBJECT_DEFAULT_TOKENURL,
|
||||
clientId: HUBJECT_DEFAULT_CLIENTID,
|
||||
clientSecret: HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
},
|
||||
},
|
||||
chargingStationCA: {
|
||||
name: 'acme',
|
||||
acme: {
|
||||
env: 'staging',
|
||||
accountKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/acme_account_key.pem',
|
||||
),
|
||||
email: 'test@citrineos.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logLevel: 2, // debug
|
||||
maxCallLengthSeconds: 20,
|
||||
maxCachingSeconds: 30,
|
||||
ocpiServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8085,
|
||||
},
|
||||
userPreferences: {
|
||||
// None by default
|
||||
},
|
||||
});
|
||||
}
|
||||
254
tools/citrineos-core-main/apps/Server/src/config/envs/local.ts
Executable file
254
tools/citrineos-core-main/apps/Server/src/config/envs/local.ts
Executable file
@@ -0,0 +1,254 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
DEFAULT_TENANT_ID,
|
||||
defineConfig,
|
||||
HUBJECT_DEFAULT_BASEURL,
|
||||
HUBJECT_DEFAULT_CLIENTID,
|
||||
HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
HUBJECT_DEFAULT_TOKENURL,
|
||||
OCPP1_6,
|
||||
OCPP2_0_1,
|
||||
OCPP_CallAction,
|
||||
OCPP_VERSION_LIST,
|
||||
} from '@citrineos/base';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
export function createLocalConfig() {
|
||||
return defineConfig({
|
||||
env: 'development',
|
||||
centralSystem: {
|
||||
host: '::',
|
||||
port: 8080,
|
||||
},
|
||||
modules: {
|
||||
certificates: {
|
||||
endpointPrefix: '/certificates',
|
||||
responses: [
|
||||
OCPP_CallAction.CertificateSigned,
|
||||
OCPP_CallAction.DeleteCertificate,
|
||||
OCPP_CallAction.GetInstalledCertificateIds,
|
||||
OCPP_CallAction.InstallCertificate,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.Get15118EVCertificate,
|
||||
OCPP_CallAction.GetCertificateStatus,
|
||||
OCPP_CallAction.SignCertificate,
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
responses: [
|
||||
OCPP_CallAction.ChangeAvailability,
|
||||
OCPP_CallAction.ClearDisplayMessage,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.GetDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmware,
|
||||
OCPP_CallAction.Reset,
|
||||
OCPP_CallAction.SetDisplayMessage,
|
||||
OCPP_CallAction.SetNetworkProfile,
|
||||
OCPP_CallAction.TriggerMessage,
|
||||
OCPP_CallAction.UnpublishFirmware,
|
||||
OCPP_CallAction.UpdateFirmware,
|
||||
OCPP_CallAction.ChangeConfiguration,
|
||||
OCPP_CallAction.GetConfiguration,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.BootNotification,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.FirmwareStatusNotification,
|
||||
OCPP_CallAction.Heartbeat,
|
||||
OCPP_CallAction.NotifyDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmwareStatusNotification,
|
||||
],
|
||||
heartbeatInterval: 60,
|
||||
bootRetryInterval: 15,
|
||||
ocpp2_0_1: {
|
||||
unknownChargerStatus: OCPP2_0_1.RegistrationStatusEnumType.Accepted,
|
||||
getBaseReportOnPending: true,
|
||||
bootWithRejectedVariables: true,
|
||||
autoAccept: true,
|
||||
},
|
||||
ocpp1_6: {
|
||||
unknownChargerStatus: OCPP1_6.BootNotificationResponseStatus.Accepted,
|
||||
},
|
||||
endpointPrefix: '/configuration',
|
||||
},
|
||||
evdriver: {
|
||||
endpointPrefix: '/evdriver',
|
||||
enableGetChargingProfilesOnStartTransaction: true,
|
||||
responses: [
|
||||
OCPP_CallAction.CancelReservation,
|
||||
OCPP_CallAction.ClearCache,
|
||||
OCPP_CallAction.GetLocalListVersion,
|
||||
OCPP_CallAction.RequestStartTransaction,
|
||||
OCPP_CallAction.RequestStopTransaction,
|
||||
OCPP_CallAction.ReserveNow,
|
||||
OCPP_CallAction.SendLocalList,
|
||||
OCPP_CallAction.UnlockConnector,
|
||||
OCPP_CallAction.RemoteStopTransaction,
|
||||
OCPP_CallAction.RemoteStartTransaction,
|
||||
OCPP_CallAction.NotifyWebPaymentStarted,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.Authorize,
|
||||
OCPP_CallAction.ReservationStatusUpdate,
|
||||
OCPP_CallAction.VatNumberValidation,
|
||||
],
|
||||
},
|
||||
monitoring: {
|
||||
endpointPrefix: '/monitoring',
|
||||
responses: [
|
||||
OCPP_CallAction.ClearVariableMonitoring,
|
||||
OCPP_CallAction.GetVariables,
|
||||
OCPP_CallAction.SetMonitoringBase,
|
||||
OCPP_CallAction.SetMonitoringLevel,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
OCPP_CallAction.SetVariableMonitoring,
|
||||
OCPP_CallAction.SetVariables,
|
||||
],
|
||||
requests: [OCPP_CallAction.NotifyEvent],
|
||||
},
|
||||
reporting: {
|
||||
endpointPrefix: '/reporting',
|
||||
responses: [
|
||||
OCPP_CallAction.CustomerInformation,
|
||||
OCPP_CallAction.GetLog,
|
||||
OCPP_CallAction.GetReport,
|
||||
OCPP_CallAction.GetBaseReport,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
OCPP_CallAction.GetDiagnostics,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.LogStatusNotification,
|
||||
OCPP_CallAction.NotifyCustomerInformation,
|
||||
OCPP_CallAction.NotifyReport,
|
||||
OCPP_CallAction.SecurityEventNotification,
|
||||
OCPP_CallAction.NotifyMonitoringReport,
|
||||
OCPP_CallAction.DiagnosticsStatusNotification,
|
||||
],
|
||||
},
|
||||
smartcharging: {
|
||||
endpointPrefix: '/smartcharging',
|
||||
responses: [
|
||||
OCPP_CallAction.ClearChargingProfile,
|
||||
OCPP_CallAction.GetChargingProfiles,
|
||||
OCPP_CallAction.GetCompositeSchedule,
|
||||
OCPP_CallAction.SetChargingProfile,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.ClearedChargingLimit,
|
||||
OCPP_CallAction.NotifyChargingLimit,
|
||||
OCPP_CallAction.NotifyEVChargingNeeds,
|
||||
OCPP_CallAction.NotifyEVChargingSchedule,
|
||||
OCPP_CallAction.ReportChargingProfiles,
|
||||
],
|
||||
},
|
||||
tenant: {
|
||||
endpointPrefix: '/tenant',
|
||||
responses: [],
|
||||
requests: [],
|
||||
},
|
||||
transactions: {
|
||||
endpointPrefix: '/transactions',
|
||||
costUpdatedInterval: 60,
|
||||
responses: [
|
||||
OCPP_CallAction.CostUpdated,
|
||||
OCPP_CallAction.GetTransactionStatus,
|
||||
OCPP_CallAction.SetDefaultTariff,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.MeterValues,
|
||||
OCPP_CallAction.StatusNotification,
|
||||
OCPP_CallAction.TransactionEvent,
|
||||
OCPP_CallAction.MeterValues,
|
||||
OCPP_CallAction.StartTransaction,
|
||||
OCPP_CallAction.StopTransaction,
|
||||
OCPP_CallAction.NotifySettlement,
|
||||
],
|
||||
},
|
||||
},
|
||||
util: {
|
||||
cache: {
|
||||
memory: true,
|
||||
},
|
||||
messageBroker: {
|
||||
amqp: {
|
||||
url: 'amqp://guest:guest@localhost:5672',
|
||||
exchange: 'citrineos',
|
||||
},
|
||||
},
|
||||
authProvider: {
|
||||
localByPass: true,
|
||||
},
|
||||
swagger: {
|
||||
path: '/docs',
|
||||
logoPath: path.resolve(path.dirname(__filename), '../../assets/logo.png'),
|
||||
exposeData: true,
|
||||
exposeMessage: true,
|
||||
},
|
||||
networkConnection: {
|
||||
websocketServers: [
|
||||
{
|
||||
id: '0',
|
||||
securityProfile: 0,
|
||||
allowUnknownChargingStations: true,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8081,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
securityProfile: 1,
|
||||
allowUnknownChargingStations: false,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8082,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
certificateAuthority: {
|
||||
v2gCA: {
|
||||
name: 'hubject',
|
||||
hubject: {
|
||||
baseUrl: HUBJECT_DEFAULT_BASEURL,
|
||||
tokenUrl: HUBJECT_DEFAULT_TOKENURL,
|
||||
clientId: HUBJECT_DEFAULT_CLIENTID,
|
||||
clientSecret: HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
},
|
||||
},
|
||||
chargingStationCA: {
|
||||
name: 'acme',
|
||||
acme: {
|
||||
env: 'staging',
|
||||
accountKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/acme_account_key.pem',
|
||||
),
|
||||
email: 'test@citrineos.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logLevel: 2, // debug
|
||||
maxCallLengthSeconds: 30,
|
||||
maxCachingSeconds: 30,
|
||||
ocpiServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8085,
|
||||
},
|
||||
userPreferences: {
|
||||
// None by default
|
||||
},
|
||||
});
|
||||
}
|
||||
269
tools/citrineos-core-main/apps/Server/src/config/envs/swarm.docker.ts
Executable file
269
tools/citrineos-core-main/apps/Server/src/config/envs/swarm.docker.ts
Executable file
@@ -0,0 +1,269 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {
|
||||
DEFAULT_TENANT_ID,
|
||||
defineConfig,
|
||||
HUBJECT_DEFAULT_BASEURL,
|
||||
HUBJECT_DEFAULT_CLIENTID,
|
||||
HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
HUBJECT_DEFAULT_TOKENURL,
|
||||
OCPP1_6,
|
||||
OCPP2_0_1,
|
||||
OCPP2_1,
|
||||
OCPP_CallAction,
|
||||
OCPP_VERSION_LIST,
|
||||
} from '@citrineos/base';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
export function createDockerConfig() {
|
||||
return defineConfig({
|
||||
env: 'development',
|
||||
centralSystem: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
},
|
||||
modules: {
|
||||
certificates: {
|
||||
endpointPrefix: 'certificates',
|
||||
host: '0.0.0.0',
|
||||
port: 8083,
|
||||
responses: [
|
||||
OCPP_CallAction.CertificateSigned,
|
||||
OCPP_CallAction.DeleteCertificate,
|
||||
OCPP_CallAction.GetInstalledCertificateIds,
|
||||
OCPP_CallAction.InstallCertificate,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.Get15118EVCertificate,
|
||||
OCPP_CallAction.GetCertificateStatus,
|
||||
OCPP_CallAction.SignCertificate,
|
||||
],
|
||||
},
|
||||
configuration: {
|
||||
responses: [
|
||||
OCPP_CallAction.ChangeAvailability,
|
||||
OCPP_CallAction.ClearDisplayMessage,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.GetDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmware,
|
||||
OCPP_CallAction.Reset,
|
||||
OCPP_CallAction.SetDisplayMessage,
|
||||
OCPP_CallAction.SetNetworkProfile,
|
||||
OCPP_CallAction.TriggerMessage,
|
||||
OCPP_CallAction.UnpublishFirmware,
|
||||
OCPP_CallAction.UpdateFirmware,
|
||||
OCPP_CallAction.ChangeConfiguration,
|
||||
OCPP_CallAction.GetConfiguration,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.BootNotification,
|
||||
OCPP_CallAction.DataTransfer,
|
||||
OCPP_CallAction.FirmwareStatusNotification,
|
||||
OCPP_CallAction.Heartbeat,
|
||||
OCPP_CallAction.NotifyDisplayMessages,
|
||||
OCPP_CallAction.PublishFirmwareStatusNotification,
|
||||
],
|
||||
heartbeatInterval: 60,
|
||||
bootRetryInterval: 15,
|
||||
ocpp2_0_1: {
|
||||
unknownChargerStatus: OCPP2_0_1.RegistrationStatusEnumType.Accepted,
|
||||
getBaseReportOnPending: true,
|
||||
bootWithRejectedVariables: true,
|
||||
autoAccept: true,
|
||||
},
|
||||
ocpp2_1: {
|
||||
unknownChargerStatus: OCPP2_1.RegistrationStatusEnumType.Accepted,
|
||||
getBaseReportOnPending: true,
|
||||
bootWithRejectedVariables: true,
|
||||
autoAccept: true,
|
||||
},
|
||||
ocpp1_6: {
|
||||
unknownChargerStatus: OCPP1_6.BootNotificationResponseStatus.Accepted,
|
||||
},
|
||||
endpointPrefix: 'configuration',
|
||||
host: '0.0.0.0',
|
||||
port: 8084,
|
||||
},
|
||||
evdriver: {
|
||||
endpointPrefix: 'evdriver',
|
||||
host: '0.0.0.0',
|
||||
port: 8085,
|
||||
enableGetChargingProfilesOnStartTransaction: true,
|
||||
responses: [
|
||||
OCPP_CallAction.CancelReservation,
|
||||
OCPP_CallAction.ClearCache,
|
||||
OCPP_CallAction.GetLocalListVersion,
|
||||
OCPP_CallAction.RequestStartTransaction,
|
||||
OCPP_CallAction.RequestStopTransaction,
|
||||
OCPP_CallAction.ReserveNow,
|
||||
OCPP_CallAction.SendLocalList,
|
||||
OCPP_CallAction.UnlockConnector,
|
||||
OCPP_CallAction.RemoteStopTransaction,
|
||||
OCPP_CallAction.RemoteStartTransaction,
|
||||
OCPP_CallAction.NotifyWebPaymentStarted,
|
||||
],
|
||||
requests: [OCPP_CallAction.Authorize, OCPP_CallAction.ReservationStatusUpdate],
|
||||
},
|
||||
monitoring: {
|
||||
endpointPrefix: 'monitoring',
|
||||
host: '0.0.0.0',
|
||||
port: 8086,
|
||||
responses: [
|
||||
OCPP_CallAction.ClearVariableMonitoring,
|
||||
OCPP_CallAction.GetVariables,
|
||||
OCPP_CallAction.SetMonitoringBase,
|
||||
OCPP_CallAction.SetMonitoringLevel,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
OCPP_CallAction.SetVariableMonitoring,
|
||||
OCPP_CallAction.SetVariables,
|
||||
],
|
||||
requests: [OCPP_CallAction.NotifyEvent],
|
||||
},
|
||||
reporting: {
|
||||
endpointPrefix: 'reporting',
|
||||
host: '0.0.0.0',
|
||||
port: 8087,
|
||||
responses: [
|
||||
OCPP_CallAction.CustomerInformation,
|
||||
OCPP_CallAction.GetLog,
|
||||
OCPP_CallAction.GetReport,
|
||||
OCPP_CallAction.GetBaseReport,
|
||||
OCPP_CallAction.GetMonitoringReport,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.LogStatusNotification,
|
||||
OCPP_CallAction.NotifyCustomerInformation,
|
||||
OCPP_CallAction.NotifyReport,
|
||||
OCPP_CallAction.SecurityEventNotification,
|
||||
OCPP_CallAction.NotifyMonitoringReport,
|
||||
],
|
||||
},
|
||||
smartcharging: {
|
||||
endpointPrefix: 'smartcharging',
|
||||
host: '0.0.0.0',
|
||||
port: 8088,
|
||||
responses: [
|
||||
OCPP_CallAction.ClearChargingProfile,
|
||||
OCPP_CallAction.GetChargingProfiles,
|
||||
OCPP_CallAction.GetCompositeSchedule,
|
||||
OCPP_CallAction.SetChargingProfile,
|
||||
],
|
||||
requests: [
|
||||
OCPP_CallAction.ClearedChargingLimit,
|
||||
OCPP_CallAction.NotifyChargingLimit,
|
||||
OCPP_CallAction.NotifyEVChargingNeeds,
|
||||
OCPP_CallAction.NotifyEVChargingSchedule,
|
||||
OCPP_CallAction.ReportChargingProfiles,
|
||||
],
|
||||
},
|
||||
tenant: {
|
||||
endpointPrefix: 'tenant',
|
||||
host: '0.0.0.0',
|
||||
port: 8090,
|
||||
responses: [],
|
||||
requests: [],
|
||||
},
|
||||
transactions: {
|
||||
endpointPrefix: 'transactions',
|
||||
host: '0.0.0.0',
|
||||
port: 8089,
|
||||
responses: [OCPP_CallAction.CostUpdated, OCPP_CallAction.GetTransactionStatus],
|
||||
requests: [
|
||||
OCPP_CallAction.MeterValues,
|
||||
OCPP_CallAction.StatusNotification,
|
||||
OCPP_CallAction.TransactionEvent,
|
||||
OCPP_CallAction.MeterValues,
|
||||
OCPP_CallAction.StartTransaction,
|
||||
OCPP_CallAction.StopTransaction,
|
||||
OCPP_CallAction.NotifySettlement,
|
||||
],
|
||||
},
|
||||
},
|
||||
util: {
|
||||
cache: {
|
||||
redis: {
|
||||
host: 'redis',
|
||||
port: 6379,
|
||||
},
|
||||
},
|
||||
messageBroker: {
|
||||
amqp: {
|
||||
url: 'amqp://guest:guest@amqp-broker:5672',
|
||||
exchange: 'citrineos',
|
||||
},
|
||||
},
|
||||
swagger: {
|
||||
path: '/docs',
|
||||
logoPath: path.resolve(path.dirname(__filename), '../../assets/certificates/logo.png'),
|
||||
exposeData: true,
|
||||
exposeMessage: true,
|
||||
},
|
||||
authProvider: {
|
||||
localByPass: true,
|
||||
},
|
||||
networkConnection: {
|
||||
websocketServers: [
|
||||
{
|
||||
id: '0',
|
||||
securityProfile: 0,
|
||||
allowUnknownChargingStations: true,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8081,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
securityProfile: 1,
|
||||
allowUnknownChargingStations: false,
|
||||
pingInterval: 60,
|
||||
host: '0.0.0.0',
|
||||
port: 8082,
|
||||
protocols: OCPP_VERSION_LIST,
|
||||
tenantId: DEFAULT_TENANT_ID,
|
||||
dynamicTenantResolution: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
certificateAuthority: {
|
||||
v2gCA: {
|
||||
name: 'hubject',
|
||||
hubject: {
|
||||
baseUrl: HUBJECT_DEFAULT_BASEURL,
|
||||
tokenUrl: HUBJECT_DEFAULT_TOKENURL,
|
||||
clientId: HUBJECT_DEFAULT_CLIENTID,
|
||||
clientSecret: HUBJECT_DEFAULT_CLIENTSECRET,
|
||||
},
|
||||
},
|
||||
chargingStationCA: {
|
||||
name: 'acme',
|
||||
acme: {
|
||||
env: 'staging',
|
||||
accountKeyFilePath: path.resolve(
|
||||
path.dirname(__filename),
|
||||
'../../assets/certificates/acme_account_key.pem',
|
||||
),
|
||||
email: 'test@citrineos.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
logLevel: 2, // debug
|
||||
maxCallLengthSeconds: 20,
|
||||
maxCachingSeconds: 30,
|
||||
ocpiServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8085,
|
||||
},
|
||||
userPreferences: {
|
||||
// None by default
|
||||
},
|
||||
});
|
||||
}
|
||||
39
tools/citrineos-core-main/apps/Server/src/config/index.ts
Normal file
39
tools/citrineos-core-main/apps/Server/src/config/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { BootstrapConfig, SystemConfig } from '@citrineos/base';
|
||||
import { loadBootstrapConfig } from '@citrineos/base';
|
||||
import { loadSystemConfig } from './config.loader.js';
|
||||
import { createLocalConfig } from './envs/local.js';
|
||||
import { createDockerConfig } from './envs/docker.js';
|
||||
|
||||
/**
|
||||
* Get default config based on environment
|
||||
* Note: This is only used if no config exists in storage
|
||||
*/
|
||||
function getDefaultConfig(): SystemConfig {
|
||||
switch (process.env.APP_ENV) {
|
||||
case 'local':
|
||||
return createLocalConfig();
|
||||
case 'docker':
|
||||
return createDockerConfig();
|
||||
default:
|
||||
throw new Error(`Invalid APP_ENV "${process.env.APP_ENV}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Export a promise that resolves to the system configuration
|
||||
export async function getSystemConfig(bootstrapConfig: BootstrapConfig): Promise<SystemConfig> {
|
||||
try {
|
||||
return await loadSystemConfig(bootstrapConfig, getDefaultConfig());
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize system configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const systemConfig: Promise<SystemConfig> = (async () => {
|
||||
const bootstrapConfig = loadBootstrapConfig();
|
||||
return await getSystemConfig(bootstrapConfig);
|
||||
})();
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
import { loadBootstrapConfig } from '@citrineos/base';
|
||||
import 'ts-node/register';
|
||||
|
||||
export default (async () => {
|
||||
try {
|
||||
const bootstrapConfig = loadBootstrapConfig();
|
||||
|
||||
const { host, port, database, dialect, username, password, ssl } = bootstrapConfig.database;
|
||||
|
||||
console.log('[sequelize.bridge.config.ts] Loaded config for DB:', {
|
||||
host,
|
||||
port,
|
||||
database,
|
||||
dialect,
|
||||
ssl,
|
||||
});
|
||||
|
||||
return {
|
||||
username,
|
||||
password,
|
||||
database,
|
||||
host,
|
||||
port,
|
||||
dialect,
|
||||
...(ssl && { dialectOptions: { ssl } }),
|
||||
logging: true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[sequelize.bridge.config.ts] Failed to load bootstrap configuration:', error);
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,142 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ICache } from '@citrineos/base';
|
||||
import type { Sequelize } from '@citrineos/core';
|
||||
import { RabbitMQConnectionManager, WebsocketNetworkConnection } from '@citrineos/core';
|
||||
import type { ILogObj } from 'tslog';
|
||||
import { Logger } from 'tslog';
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-inadarei-api-health-check
|
||||
type CheckResult = { status: 'pass' | 'fail'; error?: string };
|
||||
export type HealthCheckResult = { status: 'pass' | 'fail'; checks: Record<string, CheckResult> };
|
||||
|
||||
export class HealthCheckService {
|
||||
private _isShuttingDown: boolean = false;
|
||||
private _notReadyAt: number | null = null;
|
||||
private readonly _notReadyThresholdMs: number;
|
||||
private readonly _logger: Logger<ILogObj>;
|
||||
|
||||
constructor(
|
||||
private readonly _networkConnection: WebsocketNetworkConnection | null | undefined,
|
||||
private readonly _connectionManager: RabbitMQConnectionManager | null | undefined,
|
||||
private readonly _cache: ICache,
|
||||
private readonly _sequelizeInstance: Sequelize,
|
||||
notReadyThresholdSeconds: number,
|
||||
logger?: Logger<ILogObj>,
|
||||
) {
|
||||
this._notReadyThresholdMs = notReadyThresholdSeconds * 1000;
|
||||
this._logger = logger
|
||||
? logger.getSubLogger({ name: 'HealthCheckService' })
|
||||
: new Logger<ILogObj>({ name: 'HealthCheckService' });
|
||||
}
|
||||
|
||||
shutdown() {
|
||||
this._isShuttingDown = true;
|
||||
}
|
||||
|
||||
async checkReadiness(): Promise<HealthCheckResult> {
|
||||
let checks: Record<string, CheckResult> = {};
|
||||
let pass = true;
|
||||
if (this._isShuttingDown) {
|
||||
checks['shutdown'] = { status: 'fail', error: 'shutting down' };
|
||||
pass = false;
|
||||
} else {
|
||||
({ checks, pass } = await this._checkConnections());
|
||||
}
|
||||
|
||||
if (!pass) {
|
||||
this._notReadyAt ??= Date.now();
|
||||
} else {
|
||||
this._notReadyAt = null;
|
||||
}
|
||||
|
||||
return { status: pass ? 'pass' : 'fail', checks };
|
||||
}
|
||||
|
||||
checkLiveness(): HealthCheckResult {
|
||||
if (this._notReadyAt !== null) {
|
||||
const durationMs = Date.now() - this._notReadyAt;
|
||||
if (durationMs > this._notReadyThresholdMs) {
|
||||
const durationSec = Math.round(durationMs / 1000);
|
||||
const thresholdSec = this._notReadyThresholdMs / 1000;
|
||||
return {
|
||||
status: 'fail',
|
||||
checks: {
|
||||
readiness: {
|
||||
status: 'fail',
|
||||
error: `Not ready for ${durationSec}s (threshold: ${thresholdSec}s)`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return { status: 'pass', checks: {} };
|
||||
}
|
||||
|
||||
private async _checkConnections(): Promise<{
|
||||
checks: Record<string, CheckResult>;
|
||||
pass: boolean;
|
||||
}> {
|
||||
const checks: Record<string, CheckResult> = {};
|
||||
let pass = true;
|
||||
|
||||
if (this._networkConnection) {
|
||||
for (const [id, server] of this._networkConnection.getHttpServers()) {
|
||||
if (server.listening) {
|
||||
checks[`websocket:${id}`] = { status: 'pass' };
|
||||
} else {
|
||||
checks[`websocket:${id}`] = { status: 'fail', error: 'server not listening' };
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._connectionManager) {
|
||||
if (this._connectionManager.isConnected()) {
|
||||
checks['rabbitmq'] = { status: 'pass' };
|
||||
} else {
|
||||
checks['rabbitmq'] = { status: 'fail', error: 'not connected' };
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
|
||||
checks['cache'] = await this._checkCache();
|
||||
if (checks['cache'].status === 'fail') pass = false;
|
||||
|
||||
checks['database'] = await this._checkDatabase();
|
||||
if (checks['database'].status === 'fail') pass = false;
|
||||
|
||||
return { checks, pass };
|
||||
}
|
||||
|
||||
private async _checkCache(): Promise<CheckResult> {
|
||||
try {
|
||||
await this._cache.ping();
|
||||
return { status: 'pass' };
|
||||
} catch (error) {
|
||||
this._logger.error('Cache health check failed', { error });
|
||||
return { status: 'fail', error: 'cache unavailable' };
|
||||
}
|
||||
}
|
||||
|
||||
private _checkDatabase(): CheckResult {
|
||||
// Accessing pool is not officially supported, it has been private only for years
|
||||
// but Sequelize v6 does not provide any other way to check database connectivity without actually running a query, which we want to avoid in a health check
|
||||
const pool = (this._sequelizeInstance.connectionManager as any).pool;
|
||||
if (!pool) {
|
||||
this._logger.error('Database health check failed: pool unavailable');
|
||||
return { status: 'fail', error: 'pool unavailable' };
|
||||
}
|
||||
if (pool.size === 0 && pool.waiting > 0) {
|
||||
this._logger.error('Database health check failed: connections waiting but pool empty', {
|
||||
waiting: pool.waiting,
|
||||
maxSize: pool.maxSize,
|
||||
});
|
||||
return { status: 'fail', error: 'database unavailable' };
|
||||
}
|
||||
|
||||
return { status: 'pass' };
|
||||
}
|
||||
}
|
||||
26
tools/citrineos-core-main/apps/Server/src/index.ts
Normal file
26
tools/citrineos-core-main/apps/Server/src/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { EventGroup, loadBootstrapConfig } from '@citrineos/base';
|
||||
import { CitrineOSServer } from './citrineOSServer.js';
|
||||
import { getSystemConfig } from './config/index.js';
|
||||
|
||||
async function main() {
|
||||
const bootstrapConfig = loadBootstrapConfig();
|
||||
const config = await getSystemConfig(bootstrapConfig);
|
||||
const server = new CitrineOSServer(
|
||||
process.env.APP_NAME?.toLowerCase() as EventGroup,
|
||||
bootstrapConfig,
|
||||
config,
|
||||
);
|
||||
server.run().catch((error: any) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to initialize server:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user