@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.
369 lines • 18.2 kB
JavaScript
;
// Copyright (c) 2023 S44, LLC
// Copyright Contributors to the CitrineOS Project
//
// SPDX-License-Identifier: Apache 2.0
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebhookDispatcher = void 0;
const base_1 = require("@citrineos/base");
const data_1 = require("@citrineos/data");
const tslog_1 = require("tslog");
const uuid_1 = require("uuid");
class WebhookDispatcher {
constructor(subscriptionRepository, logger) {
this._identifiers = new Set();
// Structure of the maps: key = identifier, value = array of callbacks
this._onConnectionCallbacks = new Map();
this._onCloseCallbacks = new Map();
this._onMessageCallbacks = new Map();
this._sentMessageCallbacks = new Map();
this._subscriptionRepository = subscriptionRepository;
this._logger = logger
? logger.getSubLogger({ name: this.constructor.name })
: new tslog_1.Logger({ name: this.constructor.name });
setInterval(() => __awaiter(this, void 0, void 0, function* () {
yield this._refreshSubscriptions();
}), WebhookDispatcher.SUBSCRIPTION_REFRESH_INTERVAL_MS);
}
register(identifier) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
yield this._loadSubscriptionsForConnection(identifier);
yield Promise.all((_b = (_a = this._onConnectionCallbacks.get(identifier)) === null || _a === void 0 ? void 0 : _a.map((callback) => callback())) !== null && _b !== void 0 ? _b : []);
this._identifiers.add(identifier);
}
catch (error) {
this._logger.error(`Failed to register ${identifier}`, error);
}
});
}
deregister(identifier) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
yield Promise.all((_b = (_a = this._onCloseCallbacks.get(identifier)) === null || _a === void 0 ? void 0 : _a.map((callback) => callback())) !== null && _b !== void 0 ? _b : []);
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);
}
});
}
_dispatchMessageReceivedUnparsed(identifier, message, timestamp, protocol) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
try {
// UUID generated so that unparsed messages don't end up referencing each other
const messageId = (0, uuid_1.v4)();
const callAction = 'unparsed';
const origin = base_1.MessageOrigin.ChargingStation;
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', callAction],
]);
const messagePromise = data_1.OCPPMessage.create({
stationId: identifier,
correlationId: messageId,
origin: origin,
protocol: protocol,
action: null,
message: message,
timestamp: timestamp,
});
const promises = (_b = (_a = this._onMessageCallbacks.get(identifier)) === null || _a === void 0 ? void 0 : _a.map((callback) => callback(message, info))) !== null && _b !== void 0 ? _b : [];
promises.push(messagePromise);
yield Promise.all(promises);
}
catch (error) {
this._logger.error(`Failed to dispatch message received for ${identifier}`, error);
}
});
}
dispatchMessageReceived(identifier, message, timestamp, protocol, rpcMessage) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
if (!rpcMessage) {
// If rpcMessage is not provided, fallback to unparsed message handling
return this._dispatchMessageReceivedUnparsed(identifier, message, timestamp, protocol);
}
const transaction = yield data_1.OCPPMessage.sequelize.transaction();
try {
const messageTypeId = rpcMessage[0];
const messageId = rpcMessage[1];
const origin = base_1.MessageOrigin.ChargingStation;
const relatedMessage = yield data_1.OCPPMessage.findOne({
where: { stationId: identifier, correlationId: messageId },
transaction,
});
let callAction = undefined;
switch (messageTypeId) {
case base_1.MessageTypeId.Call:
try {
callAction = (0, base_1.mapToCallAction)(protocol, rpcMessage[2]);
}
catch (error) {
this._logger.warn(`Failed to map call action ${callAction} for ${messageId}`, error);
}
if (relatedMessage && !relatedMessage.action) {
// Update the related message with the correct action if it was missing
yield relatedMessage.update({ action: callAction }, { transaction });
}
break;
case base_1.MessageTypeId.CallResult:
case base_1.MessageTypeId.CallError: {
callAction = relatedMessage === null || relatedMessage === void 0 ? void 0 : relatedMessage.action;
break;
}
default:
// undefined
}
yield data_1.OCPPMessage.create({
stationId: identifier,
correlationId: messageId,
origin: origin,
protocol: protocol,
action: callAction,
message: rpcMessage,
timestamp: timestamp,
}, { transaction });
yield transaction.commit();
try {
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', callAction ? callAction : 'undefined'],
]);
const promises = (_b = (_a = this._onMessageCallbacks.get(identifier)) === null || _a === void 0 ? void 0 : _a.map((callback) => callback(message, info).catch((reason) => {
this._logger.error(`Failed to execute onMessage callback for ${identifier} with messageId ${messageId}: ${reason}`);
return false;
}))) !== null && _b !== void 0 ? _b : [];
yield Promise.all(promises);
}
catch (err) {
this._logger.error(`Failed to dispatch message received for ${identifier} : ${err}`);
}
}
catch (err) {
this._logger.error(`Failed to save message received for ${identifier}`, err);
yield transaction.rollback();
}
});
}
dispatchMessageSent(identifier, message, timestamp, protocol, rpcMessage) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b;
const transaction = yield data_1.OCPPMessage.sequelize.transaction();
try {
const messageTypeId = rpcMessage[0];
const messageId = rpcMessage[1];
const origin = base_1.MessageOrigin.ChargingStationManagementSystem;
const relatedMessage = yield data_1.OCPPMessage.findOne({
where: { stationId: identifier, correlationId: messageId },
transaction,
});
let callAction = undefined;
switch (messageTypeId) {
case base_1.MessageTypeId.Call:
try {
callAction = (0, base_1.mapToCallAction)(protocol, rpcMessage[2]);
}
catch (error) {
this._logger.warn(`Failed to map call action ${callAction} for ${messageId}`, error);
}
if (relatedMessage && !relatedMessage.action) {
// Update the related message with the correct action if it was missing
yield relatedMessage.update({ action: callAction }, { transaction });
}
break;
case base_1.MessageTypeId.CallResult:
case base_1.MessageTypeId.CallError: {
callAction = relatedMessage === null || relatedMessage === void 0 ? void 0 : relatedMessage.action;
break;
}
default:
// undefined
}
yield data_1.OCPPMessage.create({
stationId: identifier,
correlationId: messageId,
origin: origin,
protocol: protocol,
action: callAction,
message: rpcMessage,
timestamp: timestamp,
}, { transaction });
yield transaction.commit();
try {
const info = new Map([
['correlationId', messageId],
['origin', origin],
['timestamp', timestamp],
['protocol', protocol],
['action', callAction ? callAction : 'undefined'],
]);
const promises = (_b = (_a = this._sentMessageCallbacks.get(identifier)) === null || _a === void 0 ? void 0 : _a.map((callback) => callback(message, info).catch((reason) => {
this._logger.error(`Failed to execute sentMessage callback for ${identifier} with messageId ${messageId}: ${reason}`);
return false;
}))) !== null && _b !== void 0 ? _b : [];
yield Promise.all(promises);
}
catch (err) {
this._logger.error(`Failed to dispatch message sent for ${identifier} : ${err}`);
}
}
catch (err) {
this._logger.error(`Failed to save message sent for ${identifier}`, err);
yield transaction.rollback();
}
});
}
_refreshSubscriptions() {
return __awaiter(this, void 0, void 0, function* () {
if (this._identifiers.size === 0) {
return;
}
this._logger.debug(`Refreshing subscriptions for ${this._identifiers.size} identifiers`);
this._identifiers.forEach((identifier) => this._loadSubscriptionsForConnection(identifier));
});
}
/**
* Loads all subscriptions for a given connection into memory
*
* @param {string} connectionIdentifier - the identifier of the connection
* @return {Promise<void>} a promise that resolves once all subscriptions are loaded
*/
_loadSubscriptionsForConnection(connectionIdentifier) {
return __awaiter(this, void 0, void 0, function* () {
const onConnectionCallbacks = [];
const onCloseCallbacks = [];
const onMessageCallbacks = [];
const sentMessageCallbacks = [];
const subscriptions = yield this._subscriptionRepository.readAllByStationId(connectionIdentifier);
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}`);
}
}
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 (message, info) => __awaiter(this, void 0, void 0, function* () {
if (!subscription.messageRegexFilter ||
new RegExp(subscription.messageRegexFilter).test(message)) {
return this._subscriptionCallback({
stationId: subscription.stationId,
event: 'message',
origin: base_1.MessageOrigin.ChargingStation,
message: message,
info: info ? Object.fromEntries(info) : info,
}, subscription.url);
}
else {
// Ignore
return true;
}
});
}
_onMessageSentCallback(subscription) {
return (message, info) => __awaiter(this, void 0, void 0, function* () {
if (!subscription.messageRegexFilter ||
new RegExp(subscription.messageRegexFilter).test(message)) {
return this._subscriptionCallback({
stationId: subscription.stationId,
event: 'message',
origin: base_1.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
*/
_subscriptionCallback(requestBody, url) {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = yield 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;
}
});
}
}
exports.WebhookDispatcher = WebhookDispatcher;
WebhookDispatcher.SUBSCRIPTION_REFRESH_INTERVAL_MS = 3 * 60 * 1000;
//# sourceMappingURL=webhook.dispatcher.js.map