smart-nodes
Version:
564 lines (460 loc) • 19.9 kB
JavaScript
module.exports = function (RED)
{
"use strict";
function DimmerControlNode(config)
{
const node = this;
RED.nodes.createNode(node, config);
// ###################
// # Class constants #
// ###################
// #######################
// # Global help objects #
// #######################
const smart_context = require("../persistence.js")(RED);
const helper = require("../smart_helper.js");
// ############################
// # Used from text-exec node #
// ############################
if (typeof config.exec_text_names == "string")
node.exec_text_names = config.exec_text_names.split(",").map(n => n.trim().toLowerCase());
else
node.exec_text_names = [];
// #####################
// # persistent values #
// #####################
var node_settings = helper.cloneObject({
last_on_value: 0.0,
last_value: 0.0,
last_value_before_alarm: 0.0,
last_value_sended: 0.0,
alarm_active: false,
}, smart_context.get(node.id));
// ##################
// # Dynamic config #
// ##################
let time_total = helper.getTimeInMs(config.time_total, config.time_total_unit);
let long_press_ms = parseInt(config.long_press_ms || 1000, 10);
let update_interval = parseInt(config.update_interval || 500, 10);
let alarm_on_action = config.alarm_on_action || "NOTHING";
let alarm_on_percentage = parseFloat(config.alarm_on_percentage) || 0.0;
let alarm_off_action = config.alarm_off_action || "NOTHING";
let alarm_off_percentage = parseFloat(config.alarm_off_percentage) || 0.0;
// ##################
// # Runtime values #
// ##################
// Here the setTimeout return value is stored to turn off the dimmer.
// That means if it is null, the dimmer will not be turned off automatically.
let timeout = null;
// If isPermanent is true, then a default on time value is ignored
// Also if the motion sensor turns off, no timeout is started.
let isPermanent = false;
// If this is on, a motion sensor has detected a move, so the on time value is ignored.
// The timeout starts only if the motion sensor goes off.
let isMotion = false;
// If this is on, ignore all state changes and don't restart any time measurement.
let isBlinking = false;
// Here the date is stored, when the dimmer should go off.
// This is used to calculate the node status.
let date_turn_off = null;
// Here the setTimeout return value is stored when the toggle_dim button was pressed first.
let timeout_button_pressed = null;
// Here the date is stored, when the dimming was started
let dim_start_date = null;
// Here the value is stored, when the dimming was started
let dim_start_value = null;
// Here the dimming direction is stored, true = higher, false = lower
let dim_direction = false;
// Here the setInterval return interval ist stored, which is used for the dimming process
let interval_dim = null;
// Here the new dim value is saved for the status
let new_dim_value = null;
// #########################
// # Central node handling #
// #########################
var event = "node:" + node.id;
var handler = function (msg)
{
node.receive(msg);
}
RED.events.on(event, handler);
// ###############
// # Node events #
// ###############
node.on("input", function (msg)
{
handleTopic(msg);
setStatus();
smart_context.set(node.id, node_settings);
});
node.on("close", function ()
{
stopAutoOff();
RED.events.off(event, handler);
});
// #####################
// # Private functions #
// #####################
// This is the main function which handles all topics that was received.
let handleTopic = msg =>
{
let doRestartTimer = true;
let real_topic = helper.getTopicName(msg.topic);
switch (real_topic)
{
case "status":
// Ignore if it is in blinking mode
if (isBlinking)
return;
// Make sure payload is a float
if (typeof msg.payload == "boolean")
msg.payload = msg.payload ? 100.0 : 0.0;
else
msg.payload = parseFloat(msg.payload);
// Output is already in the state of the status value and the timeout is running.
// No need to restart the timeout.
if (node_settings.last_value == msg.payload && timeout != null)
doRestartTimer = false;
node_settings.last_value = msg.payload;
break;
case "off":
// If button is released, don't handle this message
if (msg.payload === false)
return;
node_settings.last_value = 0.0;
node_settings.last_value_sended = node_settings.last_value;
break;
case "on":
// If button is released, don't handle this message
if (msg.payload === false)
return;
node_settings.last_value = 100.0;
node_settings.last_value_sended = node_settings.last_value;
break;
case "set":
// Make sure payload is a float
if (typeof msg.payload == "boolean")
msg.payload = msg.payload ? 100.0 : 0.0;
else
msg.payload = parseFloat(msg.payload);
node_settings.last_value = msg.payload;
node_settings.last_value_sended = node_settings.last_value;
break;
case "set_permanent":
// Make sure payload is a float
if (typeof msg.payload == "boolean")
msg.payload = msg.payload ? 100.0 : 0.0;
else
msg.payload = parseFloat(msg.payload);
isPermanent = msg.payload > 0;
node_settings.last_value = msg.payload;
node_settings.last_value_sended = node_settings.last_value;
break;
case "motion":
// Make sure it is bool
msg.payload = !!msg.payload;
isMotion = msg.payload;
if (msg.payload == false)
{
// It already was off, so don't turn on
if (node_settings.last_value == 0.0)
return;
// If time is set to 0, then turn off immediately
if (helper.getTimeInMsFromString(msg.time_on ?? max_time_on) == 0)
node_settings.last_value = 0.0;
}
else
{
node_settings.last_value = node_settings.last_on_value;
}
node_settings.last_value_sended = node_settings.last_value;
break;
case "alarm":
// Make sure it is bool
msg.payload = !!msg.payload;
// No alarm change, do nothing
if (node_settings.alarm_active == msg.payload)
return;
node_settings.alarm_active = msg.payload;
if (node_settings.alarm_active)
{
// Alarm turned on
node_settings.last_value_before_alarm = node_settings.last_value;
}
else
{
// Alarm turned off
switch (alarm_off_action)
{
case "NOTHING":
break;
case "ON":
node_settings.last_value = 100.0;
break;
case "OFF":
node_settings.last_value = 0.0;
break;
case "PERCENTAGE":
node_settings.last_value = alarm_off_percentage;
break;
case "LAST":
node_settings.last_value = node_settings.last_value_before_alarm;
break;
case "LAST_SENDED":
node_settings.last_value = node_settings.last_value_sended;
break;
}
}
break;
case "blink":
if (!node_settings.alarm_active)
{
isBlinking = true;
node.send({ payload: 0.0 });
setStatus();
setTimeout(
() =>
{
isBlinking = false;
if (!node_settings.alarm_active)
{
node.send({ payload: node_settings.last_value });
setStatus();
}
},
helper.getTimeInMsFromString(msg.time_on, 500)
);
}
return;
case "dim":
if (msg.payload)
startDim(msg.direction);
else
stopDim(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
break;
case "toggle_dim":
if (msg.payload)
{
if (timeout_button_pressed != null)
clearTimeout(timeout_button_pressed);
timeout_button_pressed = setTimeout(() =>
{
// Long press detected, start dimming
timeout_button_pressed = null;
startDim(msg.direction);
}, long_press_ms);
}
else
{
if (timeout_button_pressed == null)
{
// Long press finished, stop dimming
stopDim(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
}
else
{
clearTimeout(timeout_button_pressed);
timeout_button_pressed = null;
// Short press, do a toggle.
if (node_settings.last_value == 0.0)
node_settings.last_value = node_settings.last_on_value;
else
node_settings.last_value = 0.0;
node_settings.last_value_sended = node_settings.last_value;
}
}
break;
case "toggle":
default:
// If button is released, don't handle this message
if (msg.payload === false)
return;
if (node_settings.last_value == 0.0)
node_settings.last_value = node_settings.last_on_value;
else
node_settings.last_value = 0.0;
node_settings.last_value_sended = node_settings.last_value;
break;
}
if (doRestartTimer)
stopAutoOff();
// Check alarm values
if (node_settings.alarm_active)
{
isPermanent = false;
switch (alarm_on_action)
{
case "ON":
node_settings.last_value = 100.0;
break;
case "OFF":
node_settings.last_value = 0.0;
break;
case "PERCENTAGE":
node_settings.last_value = alarm_on_percentage;
break;
case "NOTHING":
default:
break;
}
}
if (node_settings.alarm_active || helper.getTopicName(msg.topic) != "status")
{
node.send({ payload: node_settings.last_value });
if (node_settings.last_value > 0.0)
node_settings.last_on_value = node_settings.last_value;
}
// Output is on, now
if (node_settings.last_value && doRestartTimer)
startAutoOffIfNeeded(helper.getTimeInMsFromString(msg.time_on ?? max_time_on));
notifyCentral(node_settings.last_value > 0);
}
/**
* Starts the dimming in the given direction or toggle direction if parameter is null
* @param {bool|null} direction
*/
let startDim = (direction = null) =>
{
if (direction != null || direction !== true || direction !== false || direction != "up" || direction != "down")
{
node.warn("Invalid direction received. direction = " + direction);
return;
}
stopDim();
stopAutoOff();
// Auto toggle direction
if (direction == null)
direction = !dim_direction;
// Save initial values
dim_direction = direction === true || direction == "up";
dim_start_date = new Date();
dim_start_value = node_settings.last_value;
interval_dim = setInterval(() =>
{
let time_diff_ms = (new Date()).getTime() - dim_start_date.getTime();
let delta = time_diff_ms / time_total;
new_dim_value = dim_start_value
if (dim_direction)
new_dim_value += delta;
else
new_dim_value -= delta;
node.send({ payload: new_dim_value });
setStatus();
}, update_interval);
}
/**
* This stops a running dim
*/
let stopDim = origTimeMs =>
{
if (interval_dim != null)
{
clearTimeout(interval_dim);
interval_dim = null;
// Calculate new persistant values
let time_diff_ms = (new Date()).getTime() - dim_start_date.getTime();
let delta = time_diff_ms / time_total;
let new_dim_value = dim_start_value
if (dim_direction)
new_dim_value += delta;
else
new_dim_value -= delta;
node.send({ payload: new_dim_value });
startAutoOffIfNeeded(origTimeMs);
}
}
/**
* This function sets a timeout to turn off the dimmer after the defined time is over.
* @param {*} origTimeMs The on time
*/
let startAutoOffIfNeeded = origTimeMs =>
{
// No timer when alarm is active
if (node_settings.alarm_active)
return;
let timeMs = parseInt(origTimeMs);
if (isNaN(timeMs))
{
helper.warn(this, "Invalid time_on value send: " + origTimeMs);
timeMs = max_time_on;
}
// calculate end date for status message
date_turn_off = new Date();
date_turn_off.setMilliseconds(date_turn_off.getMilliseconds() + timeMs);
// Stop if any timeout is set
stopAutoOff();
// 0 = Always on
if (timeMs <= 0 || isPermanent || isMotion || !node_settings.last_value)
return;
timeout = setTimeout(() =>
{
timeout = null;
node_settings.last_value = 0.0;
node.send({ payload: 0.0 });
notifyCentral(false);
setStatus();
smart_context.set(node.id, node_settings);
}, timeMs);
};
/**
* Stops the current running timeout
*/
let stopAutoOff = () =>
{
if (timeout != null)
{
clearTimeout(timeout);
timeout = null;
}
};
/**
* Set the current node status
*/
let setStatus = () =>
{
if (node_settings.alarm_active)
{
node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": ALARM is active" });
}
else if (isBlinking)
{
node.status({ fill: "yellow", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Blink" });
}
else if (interval_dim != null)
{
node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Dim " + helper.toFixed(new_dim_value, 0) + "%" });
}
else if (node_settings.last_value)
{
if (isPermanent || isMotion || timeout == null)
node.status({ fill: "green", shape: "dot", text: helper.getCurrentTimeForStatus() + ": On" });
else if (timeout)
node.status({ fill: "yellow", shape: "ring", text: helper.getCurrentTimeForStatus() + ": Wait " + helper.formatDateToStatus(date_turn_off, "until") + " for auto off" });
}
else
{
node.status({ fill: "red", shape: "dot", text: helper.getCurrentTimeForStatus() + ": Off" });
}
}
/**
* Notify all connected central nodes
* @param {boolean} state The state if the dimmer is on
*/
let notifyCentral = state =>
{
if (!config.links)
return;
config.links.forEach(link =>
{
helper.log(node.id + " -> " + link);
helper.log({ source: node.id, state: state });
RED.events.emit("node:" + link, { source: node.id, state: state });
});
}
// After node red restart, start also the timeout
if (node_settings.last_value)
startAutoOffIfNeeded(helper.getTimeInMsFromString(max_time_on));
setStatus();
}
RED.nodes.registerType("smart_dimmer-control", DimmerControlNode);
};