@runnane/node-red-contrib-easee
Version:
Module for streaming Easee data. Based on node-red-contrib-signalrcore
325 lines (283 loc) • 10.5 kB
JavaScript
/**
* 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";
class EaseeRestClient {
constructor(n) {
RED.nodes.createNode(this, n);
var node = this;
node.charger = n.charger;
node.site = n.site;
node.circuit = n.circuit;
node.configurationNode = n.configuration;
node.connection = RED.nodes.getNode(node.configurationNode);
// Use configuration node's logging if available, fallback to console
node.logInfo = node.connection?.logInfo || function(msg, data) { console.log(`[easee] ${msg}`, data || ''); };
node.logDebug = node.connection?.logDebug || function(msg, data) { if (node.connection?.debugLogging) console.log(`[easee] DEBUG: ${msg}`, data || ''); };
node.logError = node.connection?.logError || function(msg, error) { console.error(`[easee] ERROR: ${msg}`, error || ''); };
node.logWarn = node.connection?.logWarn || function(msg, data) { console.warn(`[easee] WARN: ${msg}`, data || ''); };
if (!node.connection) {
node.error("[easee] Missing easee configuration node");
node.status({
fill: "red",
shape: "ring",
text: "Missing configuration",
});
return;
}
// Check if the configuration node has valid credentials
if (!node.connection.isConfigurationValid || !node.connection.isConfigurationValid()) {
node.error("[easee] Configuration node is invalid - missing username or password");
node.status({
fill: "red",
shape: "ring",
text: "Invalid configuration - missing credentials",
});
return;
}
/**
* Helper func for sending sailure
* @param string url
* @param {string} method
* @param {*} error
*/
node.fail = async (url, method, error) => {
node.logError("Error in easee-rest-client:", error);
node.status({
fill: "red",
shape: "dot",
text: method + ": failed",
});
node.send({
status: "error",
topic: method + ": failed",
payload: null,
error: error,
url: url
});
return true;
};
/**
* Helper func for sending success
* @param {string} url
* @param {string} method
* @param {*} response
*/
node.ok = async (url, method, response) => {
node.status({
fill: "green",
shape: "dot",
text: method + ": ok",
});
node.send({
status: "ok",
topic: url,
payload: response,
});
return true;
};
/**
* Wrapper for easee-configuration.genericCall()
*
* @param {*} url
* @param {*} method
* @param {*} body
* @returns
*/
node.REQUEST = async (url, method = "GET", body = null) => {
node.status({
fill: "yellow",
shape: "dot",
text: method + ": sending",
});
return node.connection
.genericCall(url, method, body)
.then((response) => {
return node.ok(url, method, response);
})
.catch((error) => {
return node.fail(url, method, error);
});
};
/**
* REST API GET helper command
* @param {*} url
* @returns
*/
node.GET = async (url) => {
return node.REQUEST(url, "GET");
};
/**
* REST API POST COMMAND (wrapper)
*
* @param {string} url
* @param {*} body
* @returns
*/
node.POST = async (url, body = {}) => {
return node.REQUEST(url, "POST", body);
};
/**
* On incoming nodered message
*/
node.on("input", function (msg, send, done) {
node.charger = msg?.charger ?? n.charger;
node.site = msg?.site ?? n.site;
node.circuit = msg?.circuit ?? n.circuit;
let method = "GET";
let path = "";
let body;
let url = "";
if (msg?.payload?.method ?? false) {
method = msg.payload.method.toUpperCase();
} else if (msg?.payload?.body ?? false) {
method = "POST";
}
if (msg?.payload?.path ?? false) {
path = msg.payload.path;
} else if (msg?.command ?? false) {
path = msg.command;
}
if (msg?.payload?.body ?? false) {
body = msg.payload.body;
}
if (node[method] == undefined) {
return node.fail("error", "POST", `Invalid HTTP method: ${method}`);
}
if (path && method) {
// Run full path as defined by node-red parameters
node[method](path, body);
} else if (msg?.topic ?? false) {
// Run command as defined by topic
try {
switch (msg.topic) {
case "login":
node.connection
.ensureAuthentication()
.then((isAuthenticated) => {
if (isAuthenticated) {
return node.ok("/accounts/login/", "POST", { success: true, message: "Authentication verified" });
} else {
return node.fail("/accounts/login/", "POST", new Error("Authentication failed"));
}
})
.catch((error) => {
return node.fail("/accounts/login/", "POST", error);
});
break;
case "refresh_token":
node.connection
.doRefreshToken()
.then((json) => {
return node.ok("/accounts/refresh_token/", "POST", json);
})
.catch((error) => {
return node.fail("/accounts/refresh_token/", "POST", error);
});
break;
case "dynamic_current":
if (!node.site) {
node.error("dynamic_current failed: site missing");
return;
} else if (!node.circuit) {
node.error("dynamic_current failed: circuit missing");
return;
} else if (typeof msg.payload == "object") {
// Do POST update of circuit
node.POST(`/sites/${node.site}/circuits/${node.circuit}/dynamicCurrent`, msg.payload);
} else {
// GET circuit information
node.GET(`/sites/${node.site}/circuits/${node.circuit}/dynamicCurrent`);
}
break;
case "charger":
node.GET(`/chargers/${node.charger}?alwaysGetChargerAccessLevel=true`);
break;
case "charger_details":
node.GET(`/chargers/${node.charger}/details`);
break;
case "charger_site":
node.GET(`/chargers/${node.charger}/site`);
break;
case "charger_config":
node.GET(`/chargers/${node.charger}/config`);
break;
case "charger_session_latest":
node.GET(`/chargers/${node.charger}/sessions/latest`);
break;
case "charger_session_ongoing":
node.GET(`/chargers/${node.charger}/sessions/ongoing`);
break;
case "start_charging":
case "stop_charging":
case "pause_charging":
case "resume_charging":
case "toggle_charging":
case "reboot":
node.POST(`/chargers/${node.charger}/commands/${msg.topic}`);
break;
case "charger_state":
url = `/chargers/${node.charger}/state`;
node.connection.genericCall(url).then((json) => {
if (typeof json !== "object") {
node.error("charger_state failed");
} else {
// Parse observations
Object.keys(json).forEach((idx) => {
json[idx] = node.connection.parseObservation(
{
dataName: idx,
value: json[idx],
origValue: json[idx],
},
"name"
);
});
return node.ok(url, "GET", json);
}
}).catch((error) => {
return node.fail(url, "GET", error);
});
break;
default:
return node.fail("error", "GET", `Unknown topic ${msg.topic}`);
}
} catch (error) {
return node.fail("REST client command failed", "GET", error);
}
} else {
// Missing topic
return node.fail("error", "GET", `Missing required payload.path or topic`);
}
if (done) {
done();
}
});
}
}
RED.nodes.registerType("easee-rest-client", EaseeRestClient);
};