node-red-contrib-shelly
Version:
1,451 lines (1,209 loc) • 110 kB
JavaScript
/**
* Created by Karl-Heinz Wind
* see also https://shelly-api-docs.shelly.cloud/#common-http-api
**/
module.exports = function (RED) {
"use strict";
let config = require('./config/config.json');
const axios = require('axios').default;
const rateLimit = require("axios-rate-limit");
const cloudAxios = rateLimit(axios.create(), { maxRequests: 1, perMilliseconds: 1000, maxRPS: 1 });
const fs = require("fs");
// const { readFile } = require('fs/promises'); see #96 nodejs V19
let crypto = require('crypto');
// const crypto = require('node:crypto'); see #99 nodejs V19
const path = require("path");
// const path = require('node:path'); see #99 nodejs V19
const fastify = require('fastify');
const pkg = require('./../package.json');
RED.log.info('node-red-contrib-shelly version: v' + pkg.version);
let nonceCount = 1;
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
function isMsgPayloadValid(msg) {
let isValid = false;
if (msg !== undefined && msg.payload !== undefined && !Array.isArray(msg)) {
if (!Array.isArray(msg.payload) && !isEmpty(msg.payload)) {
isValid = true;
}
}
return isValid;
}
function isMsgPayloadValidOrArray(msg) {
let isValid = false;
if (msg !== undefined && msg.payload !== undefined && !Array.isArray(msg)) {
if (!isEmpty(msg.payload)) {
isValid = true;
}
}
return isValid;
}
function trim(str) {
let result;
if (str){
result = str.trim();
}
return result;
}
function replace(str, pattern, replacement) {
let result;
if (str){
result = str.replace(pattern, replacement);
}
return result;
}
function combineUrl(path, parameters) {
let route = path + '?';
if (parameters.charAt(0) === '&'){
parameters = parameters.substring(1);
}
route += parameters;
return route;
}
// gets all IP addresses: https://stackoverflow.com/questions/3653065/get-local-ip-address-in-node-js?page=2&tab=scoredesc#tab-top
function getIPAddresses() {
let ipAddresses = [];
let interfaces = require('os').networkInterfaces();
for (let devName in interfaces) {
let iface = interfaces[devName];
for (let i = 0; i < iface.length; i++) {
let alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
ipAddresses.push(alias.address);
}
}
}
return ipAddresses;
}
RED.httpAdmin.get("/node-red-contrib-shelly-getipaddresses", function(req, res) {
let ipAddresses = getIPAddresses();
res.json(ipAddresses);
});
RED.httpAdmin.get("/node-red-contrib-shelly-getshellyinfo", async function(req, res) {
let shellyInfo;
try {
let hostname = trim(req.query.hostname);
shellyInfo = await getShellyInfo(hostname);
let deviceType;
// Generation 1 devices are mapped to gen2+ schema
if (shellyInfo.type){
shellyInfo.gen = 1;
shellyInfo.model = shellyInfo.type;
}
let keys = Object.keys(config.devices);
for (let i = 0; i < keys.length; i++) {
let device = config.devices[i];
if (device.model === shellyInfo.model) {
shellyInfo.device = device;
break;
}
};
}
catch(error){
shellyInfo = {};
}
res.json(shellyInfo);
});
// Gets the local IP address from the node or using auto detection.
function getIPAddress(node) {
let ipAddress;
if (node.server.hostip !== undefined && node.server.hostip !== '' && node.server.hostip !== 'hostname') {
ipAddress = node.server.hostip;
}
else if (node.server.hostip === 'hostname' && node.server.hostname !== undefined && node.server.hostname !== '') {
ipAddress = node.server.hostname;
}
else {
let ipAddresses = getIPAddresses();
if (ipAddresses !== undefined && ipAddresses.length > 0) {
ipAddress = ipAddresses[0];
}
else {
node.error("Could not detect local IP address: please configure hostname.");
}
}
return ipAddress;
}
// extracts the credentials from the message and the node.
function getCredentials(node, msg){
let hostname;
let username;
let password;
if (isMsgPayloadValid(msg)){
hostname = msg.payload.hostname;
username = msg.payload.username;
password = msg.payload.password;
}
if (hostname === undefined) {
hostname = node.hostname;
}
if (username === undefined) {
username = node.credentials.username;
}
if (password === undefined) {
password = node.credentials.password;
}
let authType = node.authType;
if (authType === 'Digest') {
username = 'admin'; // see https://shelly-api-docs.shelly.cloud/gen2/General/Authentication
}
let credentials = {
hostname : hostname,
authType : authType,
username : username,
password : password,
};
return credentials;
}
// Encrypts a string using SHA-256.
function sha256(str){
let result = crypto.createHash('sha256').update(str).digest('hex');
return result;
}
// see https://shelly-api-docs.shelly.cloud/gen2/General/Authentication
// see https://github.com/axios/axios/issues/686
function getDigestAuthorization(response, credentials, config){
let authDetails = response.headers['www-authenticate'].split(', ');
let propertiesArray = authDetails.map(v => v.split('='));
let properties = new Map(propertiesArray.map(obj => [obj[0], obj[1]]));
nonceCount++; // global counter
let url = config.url;
let method = config.method;
let algorithm = properties.get('algorithm'); // TODO: check if it is still SHA-256
let username = credentials.username;
let password = credentials.password;
let realm = replace(properties.get('realm'), /"/g, '');
let authParts = [username, realm, password];
let ha1String = authParts.join(':');
let ha1 = sha256(ha1String);
let ha2String = method + ':' + url;
let ha2 = sha256(ha2String);
let nc = ('00000000' + nonceCount).slice(-8);
let nonce = replace(properties.get('nonce'), /"/g, '');
let cnonce = crypto.randomBytes(24).toString('hex');
let responseString = ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + "auth" + ":" + ha2;
let responseHash = sha256(responseString);
const authorization =
'Digest username="' + username +
'", realm="' + realm +
'", nonce="' + nonce +
'", uri="' + url +
'", cnonce="' + cnonce +
'", nc=' + nc +
', qop=auth' +
', response="' + responseHash +
'", algorithm=SHA-256';
return authorization;
}
// Gets a header with the authorization property for the request.
function getHeaders(credentials){
let headers = {};
if (credentials) {
if (credentials.authType === 'Basic') {
if (credentials.username && credentials.password) {
// Authorization is case sensitive for some devices like the TRV!
headers.Authorization = "Basic " + Buffer.from(credentials.username + ":" + credentials.password).toString("base64");
};
}
}
return headers;
}
function distinct(value, index, array) {
return array.indexOf(value) === index;
}
// Gets all device type infos for the config editor
function getDeviceTypeInfos(gen){
let deviceTypeInfos = [];
let keys = Object.keys(config.devices);
for (let i = 0; i < keys.length; i++) {
let device = config.devices[i];
if (device.gen === gen) {
let deviceTypeInfo =
{
deviceType : device.model,
description : device.name + ' - (' + device.type + ' ' + device.model + ')'
}
deviceTypeInfos.push(deviceTypeInfo);
}
};
return deviceTypeInfos;
}
// Gets the distinct models from the configuration.
function getDeviceModels(gen, type){
let foundModels = [];
let keys = Object.keys(config.devices);
for (let i = 0; i < keys.length; i++) {
let device = config.devices[i];
if (device.gen === gen) {
if (device.type === type) {
foundModels.push(device.model);
}
}
};
let models = foundModels.filter(distinct);
return models;
}
// Gets the type from the model.
function getDeviceType(model){
let deviceType;
let keys = Object.keys(config.devices);
for (let i = 0; i < keys.length; i++) {
let device = config.devices[i];
if (device.model === model) {
deviceType = device.type;
break;
}
};
return deviceType;
}
// generic REST request wrapper.
async function shellyRequestAsync(axiosInstance, method, route, params, data, credentials, timeout){
if (timeout === undefined || timeout === null){
timeout = 5003;
};
// We avoid an invalid timeout by taking a default if 0.
let requestTimeout = timeout;
if (requestTimeout <= 0){
requestTimeout = 5004;
}
let headers = getHeaders(credentials);
let baseUrl = 'http://' + credentials.hostname;
let config = {
baseURL : baseUrl,
url : route,
method : method,
params : params,
data : data,
headers : headers,
timeout: requestTimeout,
validateStatus : (status) => status === 200 || status === 401
};
let result;
const response = await axiosInstance.request(config);
if (response.status == 200){
result = response.data;
} else if (response.status == 401){
config.headers = {
'Authorization': getDigestAuthorization(response, credentials, config)
}
const digestResponse = await axiosInstance.request(config);
if (digestResponse.status == 200){
result = digestResponse.data;
}
else {
throw new Error(digestResponse.statusText + ' ' + config.url);
}
} else {
throw new Error(response.statusText + ' ' + config.url);
}
return result;
}
// Hint: the /shelly route can be accessed without authorization
async function shellyPing(node, credentials, types){
// gen 1 and gen 2 devices support this endpoint (gen 2 return the same info for /rpc/Shelly.GetDeviceInfo)
try {
let data;
let params;
let body = await shellyRequestAsync(node.axiosInstance, 'GET', '/shelly', params, data, credentials, node.pollInterval);
node.shellyInfo = body;
let requiredNodeType;
let deviceType;
// Generation 1 devices
if (node.shellyInfo.type){
deviceType = node.shellyInfo.type;
requiredNodeType = 'shelly-gen1';
} // Generation 2 devices
else if (node.shellyInfo.model && node.shellyInfo.gen === 2){
deviceType = node.shellyInfo.model;
requiredNodeType = 'shelly-gen2';
} // Generation 3 devices
else if (node.shellyInfo.model && node.shellyInfo.gen === 3){
deviceType = node.shellyInfo.model;
requiredNodeType = 'shelly-gen2'; // right now the protocol is compatible to gen 2
} // Generation 4 devices
else if (node.shellyInfo.model && node.shellyInfo.gen === 4){
deviceType = node.shellyInfo.model;
requiredNodeType = 'shelly-gen2'; // right now the protocol is compatible to gen 2
}
else {
// this can not happen right now.
requiredNodeType = 'shelly gen-type is not supported';
}
if (requiredNodeType === node.type) {
let found = false;
for (let i = 0; i < types.length; i++) {
let type = types[i];
// Generation 1 devices
if (deviceType){
found = deviceType.startsWith(type);
if (found) {
break;
}
}
}
if (found){
node.status({ fill: "green", shape: "ring", text: "Connected." });
}
else{
node.status({ fill: "red", shape: "ring", text: "Shelly type mismatch: " + deviceType + " not found in [" + types.join(",") + "]"});
node.warn("Shelly type mismatch: " + deviceType);
}
}
else {
node.status({ fill: "red", shape: "ring", text: "Wrong node type. Please use " + requiredNodeType });
node.warn("Wrong node type. Please use " + requiredNodeType);
}
}
catch (error) {
node.status({ fill: "red", shape: "ring", text: "Ping: " + error.message });
if (node.verbose) {
node.warn(error.message);
}
}
}
// checks if the device is reachable and returns the shelly info. Note that /shelly does not require any credentials.
async function getShellyInfo(hostname){
let shellyInfo;
// (gen 2 return the same info for /rpc/Shelly.GetDeviceInfo)
try {
let credentials = {
hostname : hostname
}
shellyInfo = await shellyRequestAsync(axios, 'GET', '/shelly', null, null, credentials);
}
catch (error) {
shellyInfo = {};
}
return shellyInfo;
}
// checks if the device is the configured one.
async function tryCheckDeviceType(node, types){
let success = false;
let credentials = getCredentials(node);
// (gen 2 return the same info for /rpc/Shelly.GetDeviceInfo)
try {
let shellyInfo = await shellyRequestAsync(node.axiosInstance, 'GET', '/shelly', null, null, credentials);
let requiredNodeType;
let deviceType;
// Generation 1 devices
if (shellyInfo.type){
deviceType = shellyInfo.type;
requiredNodeType = 'shelly-gen1';
} // Generation 2 devices
else if (shellyInfo.model && shellyInfo.gen === 2){
deviceType = shellyInfo.model;
requiredNodeType = 'shelly-gen2';
} // Generation 3 devices
else if (shellyInfo.model && shellyInfo.gen === 3){
deviceType = shellyInfo.model;
requiredNodeType = 'shelly-gen2'; // right now the protocol is compatible to gen 2
} // Generation 4 devices
else if (shellyInfo.model && shellyInfo.gen === 4){
deviceType = shellyInfo.model;
requiredNodeType = 'shelly-gen2'; // right now the protocol is compatible to gen 2
}
else {
// this can not happen right now.
requiredNodeType = 'shelly gen-type is not supported';
}
if (requiredNodeType === node.type) {
let found = false;
for (let i = 0; i < types.length; i++) {
let type = types[i];
if (deviceType){
found = deviceType.startsWith(type);
if (found) {
break;
}
}
}
if (found){
success = true;
node.status({ fill: "green", shape: "ring", text: "" + deviceType });
}
else{
node.status({ fill: "red", shape: "ring", text: "Shelly type mismatch: " + deviceType});
node.warn("Shelly type mismatch: " + deviceType + ". Choose correct type or if the device is not supported yet then report it here: https://github.com/windkh/node-red-contrib-shelly/issues" );
}
}
else {
node.status({ fill: "red", shape: "ring", text: "Wrong node type. Please use " + requiredNodeType });
node.warn("Wrong node type. Please use " + requiredNodeType);
}
}
catch (error) {
node.status({ fill: "yellow", shape: "ring", text: "Waiting for device..." });
if (node.verbose) {
node.warn(error.message);
}
}
return success;
}
// Starts polling the status.
function start(node, types){
if (node.hostname !== ''){
let credentials = getCredentials(node);
shellyPing(node, credentials, types);
if (node.pollInterval > 0) {
node.pollingTimer = setInterval(function() {
shellyPing(node, credentials, types);
if (node.pollStatus){
node.emit("input", {});
}
}, node.pollInterval);
}
else{
node.status({ fill: "yellow", shape: "ring", text: "Polling is turned off" });
}
}
else {
node.status({ fill: "red", shape: "ring", text: "Hostname not configured" });
}
}
async function startAsync(node, types){
start(node, types);
}
// GEN 1 --------------------------------------------------------------------------------------
// Creates a route from the input.
async function inputParserRelay1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let relay = 0;
if (command.relay !== undefined){
relay = command.relay;
}
let turn;
if (command.on !== undefined){
if (command.on == true){
turn = "on";
}
else{
turn = "off"
}
}
else if (command.turn !== undefined){
turn = command.turn;
}
let timerSeconds;
if (command.timer !== undefined){
timerSeconds = command.timer;
}
let parameters = '';
if (turn !== undefined){
parameters += "&turn=" + turn;
}
if (timerSeconds !== undefined){
parameters += "&timer=" + timerSeconds;
}
if (parameters !== '') {
route = combineUrl("/relay/" + relay, parameters);
}
}
return route;
}
// Creates a route from the input.
async function inputParserMeasure1Async(msg, node, credentials){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let relay = 0;
if (command.relay !== undefined){
relay = command.relay;
}
let turn;
if (command.on !== undefined){
if (command.on == true){
turn = "on";
}
else{
turn = "off"
}
}
else if (command.turn !== undefined){
turn = command.turn;
}
let timerSeconds;
if (command.timer !== undefined){
timerSeconds = command.timer;
}
let parameters = '';
if (turn !== undefined){
parameters += "&turn=" + turn;
}
if (timerSeconds !== undefined){
parameters += "&timer=" + timerSeconds;
}
if (parameters !== '') {
route = combineUrl("/relay/" + relay, parameters);
}
// Download EM data if required.
let emetersToDownload;
if (command.download !== undefined){
emetersToDownload = command.download;
}
// special download code for EM devices that can store historical data.
if (emetersToDownload){
let data = [];
for (let i = 0; i < emetersToDownload.length; i++) {
let emeter = emetersToDownload[i];
let downloadRoute = "/emeter/" + emeter + "/em_data.csv";
node.status({ fill: "green", shape: "ring", text: "Downloading CSV " + emeter});
try {
let timeout = 60000; // download can take very long of there is a lot of data.
let body = await shellyRequestAsync(node.axiosInstance, 'GET', downloadRoute, null, null, credentials, timeout);
data.push(body);
}
catch (error) {
node.error("Downloading CSV failed " + emeter, error);
node.status({ fill: "red", shape: "ring", text: "Downloading CSV failed " + emeter});
node.warn("Downloading CSV failed " + emeter);
}
}
node.status({ fill: "green", shape: "ring", text: "Connected."});
msg.payload = data;
node.send([null, msg]);
}
}
return route;
}
// Creates a route from the input.
async function inputParserRoller1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let roller = 0;
if (command.roller !== undefined){
roller = command.roller;
}
let go;
if (command.go !== undefined){
go = command.go;
if (command.go == "to_pos" && command.roller_pos !== undefined) {
go += "&roller_pos=" + command.roller_pos;
}
}
if (go !== undefined){
route = "/roller/" + roller + "?go=" + go;
}
// we fall back to relay mode if no valid roller command is received.
if (route === undefined)
{
let relay = 0;
if (command.relay !== undefined){
relay = command.relay;
}
let turn;
if (command.on !== undefined){
if (command.on == true){
turn = "on";
}
else{
turn = "off"
}
}
else if (command.turn !== undefined){
turn = command.turn;
}
if (turn !== undefined){
route = "/relay/" + relay + "?turn=" + turn;
}
}
}
return route;
}
// Creates a route from the input.
async function inputParserDimmer1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let light = 0;
if (command.light !== undefined){
light = command.light;
}
let turn;
if (command.on !== undefined){
if (command.on == true){
turn = "on";
}
else{
turn = "off"
}
}
else if (command.turn !== undefined){
turn = command.turn;
}
else{
// turn is undefined
}
let brightness;
if (command.brightness !== undefined){
if (command.brightness >=1 && command.brightness <= 100){
brightness = command.brightness;
} else {
brightness = 100; // Default to full brightness
}
}
let white;
if (command.white !== undefined){
if (command.white >=1 && command.white <= 100){
white = command.white;
} else {
// Default is undefined
}
}
let temperature;
if (command.temp !== undefined){
if (command.temp >=2700 && command.temp <= 6500){
temperature = command.temp;
} else {
// Default is undefined
}
}
let transition;
if (command.transition !== undefined){
if (command.transition >=0 && command.transition <= 5000){
transition = command.transition;
} else {
// Default is undefined
}
}
let timer;
if (command.timer !== undefined){
if (command.timer >=0){
timer = command.timer;
} else {
// Default is undefined
}
}
let dim;
if (command.dim !== undefined){
dim = command.dim;
}
let step;
if (command.step !== undefined){
step = command.step;
}
let parameters = '';
if (turn !== undefined){
parameters += "&turn=" + turn;
}
if (brightness !== undefined){
parameters += "&brightness=" + brightness;
}
if (white !== undefined) {
parameters += "&white=" + white;
}
if (temperature !== undefined) {
parameters += "&temp=" + temperature;
}
if (transition !== undefined) {
parameters += "&transition=" + transition;
}
if (timer !== undefined) {
parameters += "&timer=" + timer;
}
if (step !== undefined) {
parameters += "&step=" + step;
}
if (dim !== undefined) {
parameters += "&dim=" + dim;
}
if (parameters !== '') {
route = combineUrl("/light/" + light, parameters);
}
}
return route;
}
// Creates a route from the input.
async function inputParserThermostat1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let thermostat = 0;
let position;
if (command.position !== undefined){
if (command.position >=0 && command.position <= 100){
position = command.position;
} else {
// Default is undefined
}
}
let temperature;
if (command.temperature !== undefined){
if (command.temperature >=4 && command.temperature <= 31){
temperature = command.temperature;
} else {
// Default is undefined
}
}
let schedule;
if (command.schedule !== undefined){
if (command.schedule == true || command.schedule == false){
schedule = command.schedule;
}
}
let scheduleProfile;
if (command.scheduleProfile !== undefined){
if (command.scheduleProfile >= 1 || command.scheduleProfile <= 5){
scheduleProfile = command.scheduleProfile;
}
}
let boostMinutes;
if (command.boostMinutes !== undefined){
if (command.boostMinutes >= 0){
boostMinutes = command.boostMinutes;
}
}
let parameters = '';
if (position !== undefined){
parameters = "&pos=" + position;
}
if (temperature !== undefined){
parameters += "&target_t=" + temperature;
}
if (schedule !== undefined){
parameters += "&schedule=" + schedule;
}
if (scheduleProfile !== undefined){
parameters += "&schedule_profile=" + scheduleProfile;
}
if (boostMinutes !== undefined){
parameters += "&boost_minutes=" + boostMinutes;
}
if (parameters !== '') {
route = combineUrl("/thermostat/" + thermostat, parameters);
}
}
return route;
}
// Creates a route from the input.
async function inputParserSensor1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
// right now sensors do not accept input commands.
}
return route;
}
// Creates a route from the input.
async function inputParserButton1Async(msg){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let input = 0;
if (command.input !== undefined){
input = command.input;
}
let event = 'S';
if (command.event !== undefined){
event = command.event;
}
let eventCount;
if (command.eventCount !== undefined){
eventCount = command.eventCount;
}
let parameters = '';
if (event !== undefined){
parameters = "&event=" + event;
}
if (eventCount !== undefined){
parameters += "&event_cnt=" + eventCount;
}
if (parameters !== '') {
route = combineUrl("/input/" + input, parameters);
}
}
return route;
}
// Creates a route from the input.
async function inputParserRGBW1Async(msg, node, credentials){
let route;
if (isMsgPayloadValid(msg)){
let command = msg.payload;
let nodeMode = node.rgbwMode;
if (nodeMode === "color") {
let red;
if (command.red !== undefined) {
if (command.red >= 0 && command.red <= 255) {
red = command.red;
} else {
red = 255; // Default to full brightness
}
}
let green;
if (command.green !== undefined) {
if (command.green >= 0 && command.green <= 255) {
green = command.green;
} else {
green = 255; // Default to full brightness
}
}
let blue ;
if (command.blue !== undefined){
if (command.blue >= 0 && command.blue <= 255){
blue = command.blue;
} else {
blue = 255; // Default to full brightness
}
}
let white;
if (command.white !== undefined) {
if (command.white >= 0 && command.white <= 255) {
white = command.white;
} else {
white = 255; // Default to full brightness
}
}
let temperature;
if (command.temp !== undefined) {
if (command.temp >= 3000 && command.temp <= 6500) {
temperature = command.temp;
} else {
// Default is undefined
}
}
let gain;
if (command.gain !== undefined) {
if (command.gain >= 0 && command.gain <= 100) {
gain = command.gain;
} else {
gain = 100; // Default to full gain
}
}
let brightness;
if (command.brightness !== undefined) {
if (command.brightness >= 0 && command.brightness <= 100) {
brightness = command.brightness;
} else {
// Default to undefined
}
}
let effect;
if (command.effect !== undefined) {
if (command.effect >=0) {
effect = command.effect;
} else {
effect = 0 // Default to no effect
}
}
let transition;
if (command.transition !== undefined) {
if (command.transition >= 0 && command.transition <= 5000) {
transition = command.transition;
} else {
// Default is undefined
}
}
let timer;
if (command.timer !== undefined) {
if (command.timer >=0) {
timer = command.timer;
} else {
timer = 0 // Default to no timer
}
}
let turn;
if (command.on !== undefined) {
if (command.on == true) {
turn = "on";
}
else {
turn = "off"
}
}
else if (command.turn !== undefined) {
turn = command.turn;
}
else
{
turn = "on";
}
// create route
route = "/color/0?turn=" + turn;
if (gain !== undefined) {
route += "&gain=" + gain;
}
if (red !== undefined) {
route += "&red=" + red;
}
if (green !== undefined) {
route += "&green=" + green;
}
if (blue !== undefined) {
route += "&blue=" + blue;
}
if (white !== undefined) {
route += "&white=" + white;
}
if (temperature !== undefined) {
route += "&temp=" + temperature;
}
if (brightness !== undefined) {
route += "&brightness=" + brightness;
}
if (effect !== undefined) {
route += "&effect=" + effect;
}
if (transition !== undefined) {
route += "&transition=" + transition;
}
if (timer !== undefined && timer > 0) {
route += "&timer=" + timer;
}
}
else if (nodeMode === "white") {
let light = 0;
if (command.light !== undefined) {
if (command.light >=0) {
light = command.light;
} else {
light = 0 // Default to no 0
}
}
let brightness;
if (command.brightness !== undefined) {
if (command.brightness >= 0 && command.brightness <= 100) {
brightness = command.brightness;
} else {
brightness = 100; // Default to full brightness
}
}
let temperature;
if (command.temp !== undefined) {
if (command.temp >= 3000 && command.temp <= 6500) {
temperature = command.temp;
} else {
// Default is undefined
}
}
let transition;
if (command.transition !== undefined) {
if (command.transition >= 0 && command.transition <= 5000) {
transition = command.transition;
} else {
// Default is undefined
}
}
let timer;
if (command.timer !== undefined) {
if (command.timer >=0) {
timer = command.timer;
} else {
timer = 0 // Default to no timer
}
}
let turn;
if (command.on !== undefined) {
if (command.on == true) {
turn = "on";
}
else {
turn = "off"
}
}
else if (command.turn !== undefined) {
turn = command.turn;
}
else
{
turn = "on";
}
// create route
route = "/white/" + light + "?turn=" + turn;
if (brightness !== undefined) {
route += "&brightness=" + brightness;
}
if (temperature !== undefined) {
route += "&temp=" + temperature;
}
if (transition !== undefined) {
route += "&transition=" + transition;
}
if (timer !== undefined && timer > 0) {
route += "&timer=" + timer;
}
}
else {
// node mode Auto or None
}
}
return route;
}
// no operation function
function inputParserEmpty1(){
}
// Returns the input parser for the device type.
function getInputParser1(deviceType){
let result;
switch(deviceType) {
case 'Relay':
result = inputParserRelay1Async;
break;
case 'Measure':
result = inputParserMeasure1Async;
break;
case 'Roller':
result = inputParserRoller1Async;
break;
case 'Dimmer':
result = inputParserDimmer1Async;
break;
case 'Thermostat':
result = inputParserThermostat1Async;
break;
case 'Sensor':
result = inputParserSensor1Async;
break;
case 'Button':
result = inputParserButton1Async;
break;
case 'RGBW':
result = inputParserRGBW1Async;
break;
default:
result = inputParserEmpty1;
break;
}
return result;
}
// initializes a RGBW node.
async function initializerRGBW1Async(node, types){
let success = false;
let checkOK = await tryCheckDeviceType(node, types);
if (checkOK === true){
try {
let credentials = getCredentials(node);
let settingsRoute = '/settings';
let settings = await shellyRequestAsync(node.axiosInstance, 'GET', settingsRoute, null, null, credentials);
node.rgbwMode = settings.mode;
success = initializer1WebhookAsync(node, types);
}
catch (error) {
node.status({ fill: "red", shape: "ring", text: "Failed to get mode from settings."});
node.warn("Failed to get mode from settings. " + error);
}
}
return success;
}
async function initializer1(node, types){
let success = false;
let checkOK = await tryCheckDeviceType(node, types);
if (checkOK === true){
let mode = node.mode;
if (mode === 'polling'){
start(node, types);
success = true;
}
else if (mode === 'callback'){
node.error("Callback not supported for this type of device.");
node.status({ fill: "red", shape: "ring", text: "Callback not supported" });
}
else{
// nothing to do.
success = true;
}
}
return success;
}
// starts polling or installs a webhook that calls a REST callback.
async function initializer1WebhookAsync(node, types){
let success = false;
let checkOK = await tryCheckDeviceType(node, types);
if (checkOK === true){
const sender = node.hostname;
await tryUninstallWebhook1Async(node, sender); // we ignore if it failed
let mode = node.mode;
if (mode === 'polling'){
await startAsync(node, types);
success = true;
}
else if (mode === 'callback'){
let ipAddress = getIPAddress(node);
let webhookUrl = 'http://' + ipAddress + ':' + node.server.port + '/webhook';
success = await tryInstallWebhook1Async(node, webhookUrl, sender);
}
else{
// nothing to do.
success = true;
}
}
return success;
}
// Installs a webhook.
async function tryInstallWebhook1Async(node, webhookUrl, sender){
let success = false;
if (node.hostname !== ''){
node.status({ fill: "yellow", shape: "ring", text: "Installing webhook..." });
let credentials = getCredentials(node);
let hookTypes = getHookTypes1(node.deviceType);
// delete http://192.168.33.1/settings/actions?index=0&name=report_url&urls[]=
// create http://192.168.33.1/settings/actions?index=0&name=report_url&enabled=true&urls[]=http://192.168.1.4/webhook
try {
if (hookTypes[0] && hookTypes[0].action === '*'){
hookTypes = await getHookTypesFromDevice1(node);
}
if (hookTypes.length !== 0){
for (let i = 0; i < hookTypes.length; i++) {
let hookType = hookTypes[i];
let name = hookType.action;
let index = hookType.index;
let url = webhookUrl + '?data=' + name + '?' + index + '?' + sender; // note that & can not be used in gen1!!!
let deleteRoute = '/settings/actions?index=' + index + '&name=' + name + '&enabled=false&urls[]=';
try {
let timeout = node.pollInterval;
let deleteResult = await shellyRequestAsync(node.axiosInstance, 'GET', deleteRoute, null, null, credentials, timeout);
let actionsAfterDelete = deleteResult.actions[name][0];
if (actionsAfterDelete.enabled === false) {
// 1st try to set the action using the standard method
let createRoute = '/settings/actions?index=' + index + '&name=' + name + '&enabled=true&urls[]=' + url;
let createResult = await shellyRequestAsync(node.axiosInstance, 'GET', createRoute, null, null, credentials, timeout);
let actionsAfterCreate = createResult.actions[name][0];
if (actionsAfterCreate.enabled === true &&
actionsAfterCreate.urls.indexOf(url) > -1) {
node.status({ fill: "green", shape: "ring", text: "Connected." });
success = true;
}
else {
// 2nd: maybe the device supports intervals
let createRoute2 = '/settings/actions?index=' + index + '&name=' + name + '&enabled=true&urls[0][url]=' + url + '&urls[0][int]=0000-0000';
let createResult2 = await shellyRequestAsync(node.axiosInstance, 'GET', createRoute2, null, null, credentials, timeout);
let actionsAfterCreate2 = createResult2.actions[name][0];
if (actionsAfterCreate2.enabled === true) {
if (actionsAfterCreate2.urls[0].url === url) {
node.status({ fill: "green", shape: "ring", text: "Connected." });
success = true;
}
else {
console.warn("Failed to install webhook " + name + " for " + sender);
success = false;
break;
}
}
else {
console.warn("Failed to install webhook " + name + " for " + sender);
success = false;
break;
}
}
}
else {
console.warn("Failed to delete webhook " + name + " for " + sender);
success = false;
break;
}
}
catch (error) {
node.status({ fill: "yellow", shape: "ring", text: "Installing webhook...." });
}
};
}
else
{
node.status({ fill: "red", shape: "ring", text: "Device does not support callbacks" });
node.warn("Installing webhook failed (" + sender + ") " + error);
}
}
catch (error) {
if (node.verbose) {
node.warn("Instal