UNPKG

@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
"use strict"; // 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