UNPKG

@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
// 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