UNPKG

@runnane/node-red-contrib-easee

Version:

Module for streaming Easee data. Based on node-red-contrib-signalrcore

315 lines (283 loc) 9.61 kB
/** * MIT License * * Copyright (c) 2025 Jon Tungland * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * This project was initially forked from node-red-contrib-signalrcore * by Scott Page (Apache License 2.0). **/ module.exports = function (RED) { "use strict"; const signalR = require("@microsoft/signalr"); class ChargerStreamingClientNode { constructor(n) { RED.nodes.createNode(this, n); var node = this; node.charger = n.charger; node.configurationNode = n.configuration; node.responses = n.responses; node.connectionConfig = RED.nodes.getNode(node.configurationNode); node.responses = n.responses; // Use configuration node's logging if available, fallback to console node.logInfo = node.connectionConfig?.logInfo || function(msg, data) { console.log(`[easee] ${msg}`, data || ''); }; node.logDebug = node.connectionConfig?.logDebug || function(msg, data) { if (node.connectionConfig?.debugLogging) console.log(`[easee] DEBUG: ${msg}`, data || ''); }; node.logError = node.connectionConfig?.logError || function(msg, error) { console.error(`[easee] ERROR: ${msg}`, error || ''); }; node.logWarn = node.connectionConfig?.logWarn || function(msg, data) { console.warn(`[easee] WARN: ${msg}`, data || ''); }; node.options = {}; node.reconnectInterval = 3000; node.closing = false; if (!this.connectionConfig) { node.emit("erro", { err: "[easee] Missing easee account configuration node", }); node.status({ fill: "red", shape: "ring", text: "Missing configuration", }); return; } // Check if the configuration node has valid credentials if (!node.connectionConfig.isConfigurationValid || !node.connectionConfig.isConfigurationValid()) { node.emit("erro", { err: "[easee] Configuration node is invalid - missing username or password", }); node.status({ fill: "red", shape: "ring", text: "Invalid configuration - missing credentials", }); return; } node.fullReconnect = () => { node.connectionConfig .ensureAuthentication() .then((isAuthenticated) => { if (isAuthenticated) { node.startconn(); } else { node.emit("erro", { err: "Authentication failed during fullReconnect()", }); } }) .catch((e) => { node.emit("erro", { err: "Error during fullReconnect(): " + e.message, }); }); }; this.connectionConfig.on("update", (msg) => { node.status({ fill: "green", shape: "dot", text: msg.update, }); //console.log(msg); }); this.on("input", (msg, send, done) => { node.fullReconnect(); if (done) { done(); } }); this.on("opened", (event) => { node.status({ fill: "green", shape: "dot", text: RED._("node-red:common.status.connected"), event: "connect", _session: { type: "signalr", id: event.id, }, }); // send the connected msg node.send([ { _connectionId: event.id, payload: "Connected" }, null, null, ]); node.connection.send("SubscribeWithCurrentState", node.charger, true); node.connection.on("ProductUpdate", (data) => { data = node.connectionConfig.parseObservation(data); node.send([null, null, null, { payload: data }, null, null]); }); node.connection.on("ChargerUpdate", (data) => { data = node.connectionConfig.parseObservation(data); node.send([null, null, null, null, { payload: data }, null]); }); node.connection.on("CommandResponse", (data) => { node.send([null, null, null, null, null, { payload: data }]); }); }); /** * Error event */ this.on("erro", (event) => { node.logError("Error in easee-streaming-client:", event.err); node.warn(event.err); node.status({ fill: "red", shape: "ring", text: event.err, event: "error", _session: { type: "signalr", id: event.id, }, }); var errMsg = { payload: event.err }; if (event.id) errMsg._connectionId = event.id; node.send([null, errMsg, null]); }); this.on("closed", (event) => { var status; if (event.count > 0) { status = { fill: "green", shape: "dot", text: RED._("node-red:common.status.connected"), }; } else { status = { fill: "red", shape: "ring", text: RED._("node-red:common.status.disconnected"), }; } status.event = "disconnect"; status._session = { type: "signalr", id: event.id, }; node.status(status); node.send([ null, null, { _connectionId: event.id, payload: "Disconnected" }, ]); }); this.on("close", (removed, done) => { node.closing = true; node.connection.stop(); if (node.reconnectTimoutHandle) { clearTimeout(node.reconnectTimoutHandle); node.reconnectTimoutHandle = null; } if (removed) { node.removeInputNode(node); } else { // This node is being restarted } node.emit("erro", { err: "Disconnected", }); if (done) done(); }); // Connect to remote endpoint node.startconn = () => { node.closing = false; if (node.reconnectTimoutHandle) clearTimeout(node.reconnectTimoutHandle); node.reconnectTimoutHandle = null; if (!node.charger) { node.emit("erro", { err: "No charger, exiting", }); return; } if (!node.connectionConfig.accessToken) { node.emit("erro", { err: "No accessToken, waiting", }); node.reconnectTimoutHandle = setTimeout( () => node.startconn(), node.reconnectInterval ); return; } node.options.accessTokenFactory = () => node.connectionConfig.accessToken; var connection = new signalR.HubConnectionBuilder() .withUrl(node.connectionConfig.signalRpath, node.options) .configureLogging(signalR.LogLevel.Debug) .build(); node.connection = connection; // keep for closing node.handleConnection(connection); }; node.reconnect = () => { if (node.reconnectTimoutHandle) clearTimeout(node.reconnectTimoutHandle); if (node.closing) return; node.connectionConfig.ensureAuthentication().then((isAuthenticated) => { if (isAuthenticated) { node.reconnectTimoutHandle = setTimeout( () => node.startconn(), node.reconnectInterval ); } else { node.logError("Authentication failed during reconnect"); } }).catch((error) => { node.logError("Error during reconnect:", error); }); }; node.notifyOnError = (err, id) => { if (!err) return; node.emit("erro", { err: err, id: id, }); }; node.handleConnection = async (connection) => { let id = ""; try { await connection.start(); // We're connected id = connection.connectionId; node.emit("opened", { count: "", id: id, }); connection.onclose((err) => { node.emit("closed", { count: "", id: id, }); node.notifyOnError(err, id); node.reconnect(); }); } catch (err) { node.notifyOnError(err, id); node.reconnect(); } }; node.closing = false; // Start in 2 sec setTimeout(() => node.fullReconnect(), 2000); } } RED.nodes.registerType( "charger-streaming-client", ChargerStreamingClientNode ); };