@citrineos/base
Version:
The base module for OCPP v2.0.1 including all interfaces. This module is not intended to be used directly, but rather as a dependency for other modules.
347 lines • 16.8 kB
JavaScript
// SPDX-FileCopyrightText: 2025 Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache-2.0
import 'reflect-metadata';
import { Logger } from 'tslog';
import { v4 as uuidv4 } from 'uuid';
import { ErrorCode, OcppError, OCPPVersion } from '../../ocpp/rpc/message.js';
import { RequestBuilder } from '../../util/request.js';
import { CacheNamespace, createIdentifier } from '../cache/types.js';
import { EventGroup, MessageOrigin, MessageState } from '../messages/internal-types.js';
import { AS_HANDLER_METADATA } from '../modules/AsHandler.js';
import { OCPPValidator } from './OCPPValidator.js';
export class AbstractModule {
static CALLBACK_URL_CACHE_PREFIX = 'CALLBACK_URL_';
_config;
_ocppValidator;
_cache;
_handler;
_sender;
_eventGroup;
_logger;
_requests = [];
_responses = [];
startTime = Date.now();
constructor(config, cache, handler, sender, eventGroup, logger, ocppValidator) {
this._logger = this._initLogger(logger);
this._ocppValidator = ocppValidator ? ocppValidator : new OCPPValidator(logger);
this._logger.info('Initializing...');
this._config = config;
this._handler = handler;
this._sender = sender;
this._eventGroup = eventGroup;
this._cache = cache;
// Set module for proper message flow.
this.handler.module = this;
}
/**
* Getters & Setters
*/
get ocppValidator() {
return this._ocppValidator;
}
get cache() {
return this._cache;
}
get sender() {
return this._sender;
}
get handler() {
return this._handler;
}
get config() {
return this._config;
}
/**
* Sets the system configuration for the module.
*
* @param {SystemConfig} config - The new configuration to set.
*/
set config(config) {
this._config = config;
// Update all necessary settings for hot reload
this._logger.info(`Updating system configuration for ${this._eventGroup} module...`);
this._logger.settings.minLevel = this._config.logLevel;
}
/**
* Methods
*/
/**
* Handles a message with an OcppRequest or OcppResponse payload.
*
* @param {IMessage<OcppRequest | OcppResponse>} message - The message to handle.
* @param {HandlerProperties} props - Optional properties for the handler.
* @return {void} This function does not return anything.
*/
async handle(message, props) {
message.payload = this._ocppValidator.sanitizeOCPPPayload(message.payload);
switch (message.state) {
case MessageState.Request: {
const { isValid, errors } = this._ocppValidator.validateOCPPRequest(message.action, message.payload, message.protocol);
if (!isValid || errors) {
throw new OcppError(message.context.correlationId, ErrorCode.FormatViolation, 'Invalid message format', {
errors: errors,
});
}
break;
}
case MessageState.Response: {
const { isValid, errors } = this._ocppValidator.validateOCPPResponse(message.action, message.payload, message.protocol);
if (!isValid || errors) {
throw new OcppError(message.context.correlationId, ErrorCode.FormatViolation, 'Invalid message format', {
errors: errors,
});
}
await this.handleMessageApiCallback(message);
await this._cache.set(message.context.correlationId, JSON.stringify(message.payload), message.context.ocppConnectionName, this._config.maxCachingSeconds);
break;
}
default:
this._logger.error('Unknown message state', message);
throw new Error('Unknown message state: ' + message.state);
}
try {
const handlerDefinition = Reflect.getMetadata(AS_HANDLER_METADATA, this.constructor)
.filter((h) => h.protocol === message.protocol && h.action === message.action)
.pop();
if (handlerDefinition) {
await handlerDefinition.method.call(this, message, props);
}
else {
throw new OcppError(message.context.correlationId, ErrorCode.NotSupported, 'No handler found for action: ' + message.action + ' at module ' + this._eventGroup);
}
}
catch (error) {
this._logger.error('Failed handling message: ', error, message);
if (message.state === MessageState.Request) {
// CallErrors are only emitted for Calls
this._logger.error('Sending CallError to ChargingStation...');
message.origin = MessageOrigin.ChargingStationManagementSystem;
if (error instanceof OcppError) {
await this._sender.sendResponse(message, error);
}
else if (error instanceof Error) {
await this._sender.sendResponse(message, new OcppError(message.context.correlationId, ErrorCode.InternalError, 'Failed handling message: ' + error.message));
}
else {
this._logger.warn("Unknown error type, couldn't send CallError");
}
}
}
}
/**
* Interface methods.
*/
/**
* Method to handle incoming {@link IMessage}.
*
* @param message The {@link IMessage} to handle. Can contain either a {@link OcppRequest} or a {@link OcppResponse} as payload.
* @param props The {@link HandlerProperties} for this {@link IMessage} containing implementation specific metadata. Metadata is not used in the base implementation.
*/
async handleMessageApiCallback(message) {
const url = await this._cache.get(message.context.correlationId, AbstractModule.CALLBACK_URL_CACHE_PREFIX + message.context.ocppConnectionName);
if (url) {
this._logger.debug(`Sending call result to callback URL: ${url} for correlationId: ${message.context.correlationId}`);
try {
await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(message.payload),
});
}
catch (error) {
// TODO: Ideally the error log is also stored in the database in a failed invocations table to ensure these are visible outside of a log file.
this._logger.error('Failed sending call result: ', error);
}
}
}
/**
* Calls shutdown on the handler and sender.
*
* Note: To be overwritten by subclass if other logic is necessary.
*
*/
async shutdown() {
await this._handler.shutdown();
await this._sender.shutdown();
}
/**
* Default implementation
*/
/**
* Sends a call with the specified identifier, tenantId, protocol, action, payload, and origin.
*
* @param ocppConnectionName - The connection name of the charging station
* @param {number} tenantId - The identifier of the tenant.
* @param {string} protocol - The subprotocol of the Websocket, i.e. "ocpp1.6" or "ocpp2.0.1".
* @param {CallAction} action - The action to be performed.
* @param {OcppRequest} payload - The payload of the call.
* @param {string} [callbackUrl] - The callback URL for the call.
* @param {string} [correlationId] - The correlation ID of the call.
* @param {MessageOrigin} [origin] - The origin of the call.
* @return {Promise<IMessageConfirmation>} A promise that resolves to the message confirmation.
*/
async sendCall(ocppConnectionName, tenantId, protocol, action, payload, callbackUrl, correlationId, origin = MessageOrigin.ChargingStationManagementSystem) {
const identifier = createIdentifier(tenantId, ocppConnectionName);
const _correlationId = correlationId === undefined ? uuidv4() : correlationId;
payload = this._ocppValidator.sanitizeOCPPPayload(payload);
const { isValid, errors } = this._ocppValidator.validateOCPPRequest(action, payload, protocol);
if (!isValid || errors) {
throw new OcppError(_correlationId, ErrorCode.FormatViolation, 'Invalid message format', {
errors: errors,
});
}
if (callbackUrl) {
// TODO: Handle callErrors, failure to send to charger, timeout from charger, with different responses to callback
this._logger.debug(`Setting callback URL: ${callbackUrl} for correlationId: ${_correlationId}`);
this._cache
.set(_correlationId, callbackUrl, AbstractModule.CALLBACK_URL_CACHE_PREFIX + ocppConnectionName, this._config.maxCachingSeconds)
.then((value) => {
if (value) {
this._logger.debug(`Successfully set cache for correlationId: ${_correlationId}`);
}
else {
this._logger.warn(`Failed to set cache for correlationId: ${_correlationId}`);
}
})
.catch((error) => this._logger.error('Error setting cache: ', error));
}
// TODO: Future - Compound key with tenantId
return this._cache.get(identifier, CacheNamespace.Connections).then((connection) => {
if (connection) {
const websocketConnection = JSON.parse(connection);
if (websocketConnection.protocol !== protocol) {
this._logger.error(`Failed sending call. Requested protocol: '${protocol}', connection protocol: '${websocketConnection.protocol}' for identifier: `, identifier);
return Promise.resolve({
success: false,
payload: `Requested protocol: '${protocol}', connection protocol: '${websocketConnection.protocol}' for identifier: '${identifier}'`,
});
}
return this._sender.sendRequest(RequestBuilder.buildCall(ocppConnectionName, _correlationId, tenantId, action, payload, this._eventGroup, origin, protocol));
}
else {
this._logger.error('Failed sending call. No connection found for identifier: ', identifier);
return Promise.resolve({
success: false,
payload: 'No connection found for identifier: ' + identifier,
});
}
});
}
/**
* Sends the call result message and returns a Promise that resolves with the confirmation message.
*
* @param {string} correlationId - The correlation ID of the message.
* @param ocppConnectionName - The connection name of the charging station
* @param {number} tenantId - The identifier of the tenant.
* @param {string} protocol - The subprotocol of the Websocket, i.e. "ocpp1.6" or "ocpp2.0.1".
* @param {CallAction} action - The call action.
* @param {OcppResponse} payload - The payload of the call result message.
* @param {MessageOrigin} origin - (optional) The origin of the message.
* @return {Promise<IMessageConfirmation>} A Promise that resolves with the confirmation message.
*/
sendCallResult(correlationId, ocppConnectionName, tenantId, protocol, action, payload, origin = MessageOrigin.ChargingStationManagementSystem) {
payload = this._ocppValidator.sanitizeOCPPPayload(payload);
const { isValid, errors } = this._ocppValidator.validateOCPPResponse(action, payload, protocol);
if (!isValid || errors) {
throw new OcppError(correlationId, ErrorCode.FormatViolation, 'Invalid message format', {
errors: errors,
});
}
return this._sender.sendResponse(RequestBuilder.buildCallResult(ocppConnectionName, correlationId, tenantId, action, payload, this._eventGroup, origin, protocol));
}
/**
* Sends the call result using the request message's fields.
* Payload will overwrite message.payload.
*
* @param {IMessage<OcppRequest>} message - The request message object.
* @param {OcppResponse} payload - The payload to send.
* @return {Promise<IMessageConfirmation>} A promise that resolves to the message confirmation.
*/
sendCallResultWithMessage(message, payload) {
payload = this._ocppValidator.sanitizeOCPPPayload(payload);
const { isValid, errors } = this._ocppValidator.validateOCPPResponse(message.action, payload, message.protocol);
if (!isValid || errors) {
throw new OcppError(message.context.correlationId, ErrorCode.FormatViolation, 'Invalid message format', {
errors: errors,
});
}
message.origin = MessageOrigin.ChargingStationManagementSystem;
return this._sender.sendResponse(message, payload);
}
/**
* Sends the call error message and returns a Promise that resolves with the confirmation message.
*
* @param {string} correlationId - The correlation ID of the message.
* @param ocppConnectionName - The connection name of the charging station
* @param {number} tenantId - The identifier of the tenant.
* @param {string} protocol - The subprotocol of the Websocket, i.e. "ocpp1.6" or "ocpp2.0.1".
* @param {CallAction} action - The call action.
* @param {OcppError} payload - The payload of the call error message.
* @param {MessageOrigin} origin - (optional) The origin of the message.
* @return {Promise<IMessageConfirmation>} A Promise that resolves with the confirmation message.
*/
sendCallError(correlationId, ocppConnectionName, tenantId, protocol, action, payload, origin = MessageOrigin.ChargingStationManagementSystem) {
return this._sender.sendResponse(RequestBuilder.buildCallError(ocppConnectionName, correlationId, tenantId, action, payload, this._eventGroup, origin, protocol));
}
/**
* Sends the call error using the request message's fields.
* Payload will overwrite message.payload.
*
* @param {IMessage<OcppRequest>} message - The request message object.
* @param {OcppResponse} payload - The payload to send.
* @return {Promise<IMessageConfirmation>} A promise that resolves to the message confirmation.
*/
sendCallErrorWithMessage(message, payload) {
message.origin = MessageOrigin.ChargingStationManagementSystem;
return this._sender.sendResponse(message, payload);
}
/**
* Initializes the logger for the class.
*
* @return {Logger<ILogObj>} The initialized logger.
*/
_initLogger(baseLogger) {
return baseLogger
? baseLogger.getSubLogger({ name: this.constructor.name })
: new Logger({
name: this.constructor.name,
minLevel: this._config.logLevel,
hideLogPositionForProduction: this._config.env === 'production',
});
}
/**
* Initializes the handler for handling requests and responses.
*/
async initHandlers() {
const result = await this._initHandler(this._requests, this._responses);
if (!result) {
throw new Error('Could not initialize module due to failure in handler initialization.');
}
this._logger.info(`Initialized in ${Date.now() - this.startTime}ms...`);
}
/**
* Initializes the handler for handling requests and responses.
*
* @param {CallAction[]} requests - The array of call actions for requests.
* @param {CallAction[]} responses - The array of call actions for responses.
* @return {Promise<boolean>} Returns a promise that resolves to a boolean indicating if the initialization was successful.
*/
async _initHandler(requests, responses) {
this._handler.module = this;
let success = await this._handler.subscribe(this._eventGroup.toString() + '_requests', requests, {
origin: MessageOrigin.ChargingStation.toString(),
state: MessageState.Request.toString(),
});
success =
success &&
(await this._handler.subscribe(this._eventGroup.toString() + '_responses', responses, {
origin: MessageOrigin.ChargingStation.toString(),
state: MessageState.Response.toString(),
}));
return success;
}
}
//# sourceMappingURL=AbstractModule.js.map