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.

280 lines 12.8 kB
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