@citrineos/ocpprouter
Version:
The ocpprouter module for OCPP v2.0.1. This module is not intended to be used directly, but rather as a dependency for other modules.
280 lines • 12.8 kB
JavaScript
import { createIdentifier, getStationIdFromIdentifier, getTenantIdFromIdentifier, MessageOrigin, MessageState, } from '@citrineos/base';
import { Subscription } from '@citrineos/data';
import { Logger } from 'tslog';
import { v4 as uuidv4 } from 'uuid';
export class WebhookDispatcher {
static SUBSCRIPTION_REFRESH_INTERVAL_MS = 3 * 60 * 1000;
_logger;
_ocppMessageRepository;
_subscriptionRepository;
_identifiers = new Set();
// Structure of the maps: key = identifier, value = array of callbacks
_onConnectionCallbacks = new Map();
_onCloseCallbacks = new Map();
_onMessageCallbacks = new Map();
_sentMessageCallbacks = new Map();
constructor(ocppMessageRepository, subscriptionRepository, logger) {
this._ocppMessageRepository = ocppMessageRepository;
this._subscriptionRepository = subscriptionRepository;
this._logger = logger
? logger.getSubLogger({ name: this.constructor.name })
: new Logger({ name: this.constructor.name });
setInterval(async () => {
await this._refreshSubscriptions();
}, WebhookDispatcher.SUBSCRIPTION_REFRESH_INTERVAL_MS);
}
async register(tenantId, stationId) {
const identifier = createIdentifier(tenantId, stationId);
try {
await this._loadSubscriptionsForConnection(tenantId, stationId);
await Promise.all(this._onConnectionCallbacks.get(identifier)?.map((callback) => callback()) ?? []);
this._identifiers.add(identifier);
}
catch (error) {
this._logger.error(`Failed to register ${identifier}`, error);
}
}
async deregister(tenantId, stationId) {
const identifier = createIdentifier(tenantId, stationId);
try {
await Promise.all(this._onCloseCallbacks.get(identifier)?.map((callback) => callback()) ?? []);
this._identifiers.delete(identifier);
this._onConnectionCallbacks.delete(identifier);
this._onCloseCallbacks.delete(identifier);
this._onMessageCallbacks.delete(identifier);
this._sentMessageCallbacks.delete(identifier);
}
catch (error) {
this._logger.error(`Failed to deregister ${identifier}`, error);
}
}
async dispatchMessageReceivedUnparsed(tenantId, stationId, message, timestamp, protocol, action, state) {
const identifier = createIdentifier(tenantId, stationId);
try {
// UUID generated so that unparsed messages don't end up referencing each other
const messageId = uuidv4();
const origin = MessageOrigin.ChargingStation;
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', action],
]);
const messagePromise = this._ocppMessageRepository.createOCPPMessage(tenantId, {
tenantId: tenantId,
stationId: stationId,
correlationId: messageId,
origin: origin,
state: state,
protocol: protocol,
action: action,
message: message,
timestamp: timestamp,
});
const promises = this._onMessageCallbacks.get(identifier)?.map((callback) => callback(message, info)) ?? [];
promises.push(messagePromise);
await Promise.all(promises);
}
catch (error) {
this._logger.error(`Failed to dispatch message received for ${identifier}`, error);
}
}
async dispatchMessageReceived(tenantId, stationId, timestamp, protocol, action, state, rpcMessage) {
const identifier = createIdentifier(tenantId, stationId);
const messageId = rpcMessage[1];
const origin = MessageOrigin.ChargingStation;
const messageRecord = await this._ocppMessageRepository.createOCPPMessage(tenantId, {
tenantId: tenantId,
stationId: stationId,
correlationId: messageId,
origin: origin,
state: state,
action: action,
protocol: protocol,
message: rpcMessage,
timestamp: timestamp,
});
if (action === undefined) {
this._logger.debug(`Using action from stored message for correlationId ${messageId} and tenantId ${tenantId}: ${messageRecord.action}`);
action = messageRecord.action;
}
try {
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', action ? action : 'undefined'],
]);
const rawMessage = JSON.stringify(rpcMessage);
const promises = this._onMessageCallbacks.get(identifier)?.map((callback) => callback(rawMessage, info).catch((reason) => {
this._logger.error(`Failed to execute onMessage callback for ${identifier} with messageId ${messageId}: ${reason}`);
return false;
})) ?? [];
await Promise.all(promises);
}
catch (err) {
this._logger.error(`Failed to dispatch message received for ${identifier} : ${err}`);
}
}
async dispatchMessageSent(identifier, action, state, timestamp, protocol, rpcMessage) {
const tenantId = getTenantIdFromIdentifier(identifier);
const stationId = getStationIdFromIdentifier(identifier);
const messageId = rpcMessage[1];
const origin = MessageOrigin.ChargingStationManagementSystem;
const messageRecordPromise = this._ocppMessageRepository.createOCPPMessage(tenantId, {
tenantId: tenantId,
stationId: stationId,
correlationId: messageId,
origin: origin,
state: state,
action: action,
protocol: protocol,
message: rpcMessage,
timestamp: timestamp,
});
try {
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', action ? action : 'undefined'],
]);
const rawMessage = JSON.stringify(rpcMessage);
const promises = this._sentMessageCallbacks.get(identifier)?.map((callback) => callback(rawMessage, info).catch((reason) => {
this._logger.error(`Failed to execute sentMessage callback for ${identifier} with messageId ${messageId}: ${reason}`);
return false;
})) ?? [];
await Promise.all([...promises, messageRecordPromise]);
}
catch (err) {
this._logger.error(`Failed to dispatch message sent for ${identifier} : ${err}`);
}
}
async _refreshSubscriptions() {
if (this._identifiers.size === 0) {
return;
}
this._logger.debug(`Refreshing subscriptions for ${this._identifiers.size} identifiers`);
this._identifiers.forEach((identifier) => this._loadSubscriptionsForConnection(getTenantIdFromIdentifier(identifier), getStationIdFromIdentifier(identifier)));
}
/**
* Loads all subscriptions for a given connection into memory
*
* @param {number} tenantId
* @param {string} stationId
* @return {Promise<void>} a promise that resolves once all subscriptions are loaded
*/
async _loadSubscriptionsForConnection(tenantId, stationId) {
const onConnectionCallbacks = [];
const onCloseCallbacks = [];
const onMessageCallbacks = [];
const sentMessageCallbacks = [];
const subscriptions = await this._subscriptionRepository.readAllByStationId(tenantId, stationId);
for (const subscription of subscriptions) {
if (subscription.onConnect) {
onConnectionCallbacks.push(this._onConnectionCallback(subscription));
this._logger.debug(`Added onConnect callback to ${subscription.url} for station ${subscription.stationId}`);
}
if (subscription.onClose) {
onCloseCallbacks.push(this._onCloseCallback(subscription));
this._logger.debug(`Added onClose callback to ${subscription.url} for station ${subscription.stationId}`);
}
if (subscription.onMessage) {
onMessageCallbacks.push(this._onMessageReceivedCallback(subscription));
this._logger.debug(`Added onMessage callback to ${subscription.url} for station ${subscription.stationId}`);
}
if (subscription.sentMessage) {
sentMessageCallbacks.push(this._onMessageSentCallback(subscription));
this._logger.debug(`Added sentMessage callback to ${subscription.url} for station ${subscription.stationId}`);
}
}
const connectionIdentifier = createIdentifier(tenantId, stationId);
this._onConnectionCallbacks.set(connectionIdentifier, onConnectionCallbacks);
this._onCloseCallbacks.set(connectionIdentifier, onCloseCallbacks);
this._onMessageCallbacks.set(connectionIdentifier, onMessageCallbacks);
this._sentMessageCallbacks.set(connectionIdentifier, sentMessageCallbacks);
}
_onConnectionCallback(subscription) {
return (info) => this._subscriptionCallback({
stationId: subscription.stationId,
event: 'connected',
info: info ? Object.fromEntries(info) : info,
}, subscription.url);
}
_onCloseCallback(subscription) {
return (info) => this._subscriptionCallback({
stationId: subscription.stationId,
event: 'closed',
info: info ? Object.fromEntries(info) : info,
}, subscription.url);
}
_onMessageReceivedCallback(subscription) {
return async (message, info) => {
if (!subscription.messageRegexFilter ||
new RegExp(subscription.messageRegexFilter).test(message)) {
return this._subscriptionCallback({
stationId: subscription.stationId,
event: 'message',
origin: MessageOrigin.ChargingStation,
message: message,
info: info ? Object.fromEntries(info) : info,
}, subscription.url);
}
else {
// Ignore
return true;
}
};
}
_onMessageSentCallback(subscription) {
return async (message, info) => {
if (!subscription.messageRegexFilter ||
new RegExp(subscription.messageRegexFilter).test(message)) {
return this._subscriptionCallback({
stationId: subscription.stationId,
event: 'message',
origin: MessageOrigin.ChargingStationManagementSystem,
message: message,
info: info ? Object.fromEntries(info) : info,
}, subscription.url);
}
else {
// Ignore
return true;
}
};
}
/**
* Sends a message to a given URL that has been subscribed to a station connection event
*
* @param {Object} requestBody - request body containing stationId, event, origin, message, error, and info
* @param {string} url - the URL to fetch data from
* @return {Promise<boolean>} a Promise that resolves to a boolean indicating success
*/
async _subscriptionCallback(requestBody, url) {
try {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
this._logger.error(`Route to subscription ${url} on charging station ${requestBody.stationId} failed.
Event: ${requestBody.event}, ${response.status} ${response.statusText} - ${errorText}`);
}
return response.ok;
}
catch (error) {
this._logger.error(`Route to subscription ${url} on charging station ${requestBody.stationId} failed.
Event: ${requestBody.event}, ${error}`);
return false;
}
}
}
//# sourceMappingURL=webhook.dispatcher.js.map