node-red-dashboard-2-t86
Version:
Set of Node-RED nodes to controll home automation based on Unipi Patron and DALI.
410 lines (409 loc) • 15.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = main;
const foxtron_types_1 = require("../foxtron-types");
const foxtron_serial_frame_1 = require("../foxtron-serial-frame");
const SEC_MS = 1000;
var Key;
(function (Key) {
Key["LastState"] = "LAST_STATE";
})(Key || (Key = {}));
var Event;
(function (Event) {
Event["Off"] = "off";
Event["Max"] = "max";
Event["Toggle"] = "toggle";
Event["FadeUp"] = "up";
Event["FadeDown"] = "down";
Event["FadeStop"] = "stop";
Event["SetLevel"] = "set";
Event["Query"] = "query";
Event["Reset"] = "reset"; // Reset ballast state after flow initialization
})(Event || (Event = {}));
var FadeAction;
(function (FadeAction) {
FadeAction[FadeAction["None"] = 0] = "None";
FadeAction[FadeAction["Up"] = 1] = "Up";
FadeAction[FadeAction["Down"] = 2] = "Down";
})(FadeAction || (FadeAction = {}));
// Helper metod to process event from given string or object
// It searches topic and event keys. If they aren't present input string is used instead.
const eventWithString = (str) => {
if (str && str.event)
str = str.event;
if (typeof str !== 'string')
return Event.Query;
return Object.values(Event).includes(str) ? str : Event.Query;
};
// Helper to parse value from payload
const intWithPayload = (pld) => {
if (pld && pld.value)
pld = pld.value;
if (typeof pld.state === 'boolean' && !pld.state) {
return 0;
}
if (typeof pld.level === 'number') {
pld = pld.level;
}
if (typeof pld === 'string')
pld = parseInt(pld);
if (typeof pld === 'number' && !isNaN(pld))
return pld;
return null;
};
// Helper to parse ballast address from editor config
// Usually called only during init
const addressResolv = (config) => {
const addr = {
type: foxtron_types_1.AddressType.Broadcast,
value: 0
};
if (config && typeof config.addresstype !== 'undefined') {
const atype = (typeof config.addresstype === 'string') ? parseInt(config.addresstype) : config.addresstype;
if (!isNaN(atype) && Object.values(foxtron_types_1.AddressType).includes(atype)) {
addr.type = atype;
if (addr.type !== foxtron_types_1.AddressType.Broadcast
&& config.addressval !== null
&& typeof config.addressval !== 'undefined') {
addr.value = (typeof config.addressval === 'string') ? parseInt(config.addressval) : config.addressval;
if (isNaN(addr.value)) {
addr.value = 0;
}
}
}
}
return addr;
};
// Helper to parse ballast level bounds from editor config
// Usually called only during init
const levelBoundsResolv = (config) => {
const bl = {
min: parseInt(config.minlevel),
max: parseInt(config.maxlevel)
};
if (typeof bl.min === 'undefined'
|| isNaN(bl.min)
|| bl.min < foxtron_types_1.MIN_DALI_LEVEL)
bl.min = foxtron_types_1.MIN_DALI_LEVEL;
if (typeof bl.max === 'undefined'
|| isNaN(bl.max)
|| bl.max > foxtron_types_1.MAX_DALI_LEVEL)
bl.min = foxtron_types_1.MAX_DALI_LEVEL;
return bl;
};
// Helper to parse fade rate from editor config
// Usually called only during init. The returned fade rate is value from DALI command (1 - 15)
const fadeRateResolv = (config) => {
let fadeRate = parseInt(config.faderate);
if (!fadeRate
|| isNaN(fadeRate)
|| fadeRate < foxtron_types_1.DALI_MIN_FADE_RATE_N)
fadeRate = foxtron_types_1.DALI_MIN_FADE_RATE_N;
if (fadeRate > foxtron_types_1.DALI_MAX_FADE_RATE_N)
fadeRate = foxtron_types_1.DALI_MAX_FADE_RATE_N;
return fadeRate;
};
// Helper to calculate how many levels should be faded per 1 step.
// Step is 200 ms, input is fade rate from DALI (1 - 15). It's converted to levels per
// second and then adjusted to 1 step (200ms)
const calcFadeLevelsPerStep = (rate) => {
return ((0, foxtron_types_1.fadeRateSteps)(rate) || 1) / SEC_MS * foxtron_types_1.DALI_FADE_STEP_DURATION_MS;
};
// Helper to check if 2 parsed editor configs are equal
function parsetConfigEqual(pc1, pc2) {
return (pc1.fadeRate === pc2.fadeRate
&& pc1.levelBounds.min === pc2.levelBounds.min
&& pc1.levelBounds.max === pc2.levelBounds.max
&& pc1.address.type === pc2.address.type
&& (pc1.address.type !== foxtron_types_1.AddressType.Broadcast || pc1.address.value === pc2.address.value));
}
// Main class of FoxtronDaliBallast. Each node in Node RED is represented by it's instance
class FoxtronDaliBallast {
constructor(RED, node, config) {
// TODO - query all info about the ballast and return it in structured manner
this.queryBallastState = (send) => {
throw new Error('Query Ballast state not implementd');
};
// Set specific level. Uses DACP. If requested level is out of ballast bound min or max
// level is used.
// Level 0 means to switch the ballast off.
// Difference between cmd Off/Max and setLevel is the transition to level set by setLevel is
// smooth (uses DALI Fade Time) where Off/Max is instant.
this.setLevel = (val, send) => {
console.log(`Set level ${val}`);
if (this.stopFade(send)) {
return;
}
if (val > this.pConf.levelBounds.max)
val = this.pConf.levelBounds.max;
if (val !== 0) {
if (val < this.pConf.levelBounds.min)
val = this.pConf.levelBounds.min;
}
if (val === 0) {
this.sendToBallast({
opcode: foxtron_types_1.Opcode.OFF,
address: this.pConf.address
}, send);
}
else {
this.sendToBallast({
opcode: foxtron_types_1.Opcode.DAPC,
address: this.pConf.address,
value: val
}, send);
}
this.level = val;
this.isOn = !!val;
};
this.RED = RED;
this.node = node;
RED.nodes.createNode(node, config);
// Parse node config set in settings pane
const fr = fadeRateResolv(config);
this.pConf = {
address: addressResolv(config),
levelBounds: levelBoundsResolv(config),
fadeRate: fr,
fadeLevelPerStep: calcFadeLevelsPerStep(fr)
};
// Load last state and if it's there and config in it doesn't match the parsed one
// get rid of the last state
this.nctx = this.node.context();
this.refreshState();
// Fade loop is allways reset on init. + schedule loop execution
this.resetFade();
this.node.on('input', this.onInput.bind(this));
this.node.on('close', this.onClose.bind(this));
}
// Refresh node state from the context storage
// Initialize a default state if there is no saved state or if config changed
refreshState() {
let saved = this.nctx.get(Key.LastState);
if (!saved || !saved.config || !parsetConfigEqual(this.pConf, saved.config)) {
saved = {
level: 0,
state: false,
lastDaliLevel: 0,
config: this.pConf
};
}
this.level = saved.level > 0 ? saved.level : saved.lastDaliLevel;
this.isOn = saved.state;
this.storeState();
}
// Return serialized state of the current node
serializeState() {
return {
level: this.isOn ? this.level : 0,
state: this.isOn,
lastDaliLevel: this.level,
config: this.pConf
};
}
// Store serialized state to context storage
storeState() {
const color = this.isOn ? 'green' : 'red';
this.node.status({ fill: color, shape: "ring", text: `Level: ${this.level}` });
this.nctx.set(Key.LastState, this.serializeState());
}
// Reset fade properties to desired state
resetFade(fa, lvl) {
if (this.fadeLoopInterval) {
clearInterval(this.fadeLoopInterval);
this.fadeLoopInterval = null;
}
this.fadeAction = (typeof fa === 'undefined') ? FadeAction.None : fa;
this.fadeStepCntr = 0;
this.fadeStartLevel = (typeof lvl === 'undefined') ? 0 : lvl;
if (this.fadeAction !== FadeAction.None) {
setTimeout(this.fadeLoopTick.bind(this), 1);
this.fadeLoopInterval = setInterval(this.fadeLoopTick.bind(this), foxtron_types_1.DALI_FADE_STEP_DURATION_MS);
}
}
// Handle incoming message
onInput(msg, send, done) {
// Refresh state from the context
this.refreshState();
console.log(msg);
// Event might be in topic or in payload, some events might have value. It's in
// payload or payload.value.
const event = eventWithString(msg.payload);
const value = event === Event.SetLevel ? intWithPayload(msg.payload) : null;
console.log(`event ${event}, value ${value}`);
// Handle event
switch (event) {
case Event.Off:
this.setOff(send);
break;
case Event.Max:
this.setMax(send);
break;
case Event.Toggle:
this.toggle(send);
break;
case Event.FadeUp:
this.startFadeUp(send);
break;
case Event.FadeDown:
this.startFadeDown(send);
break;
case Event.FadeStop:
this.stopFade(send);
break;
case Event.Reset:
this.resetBallastState(send);
break;
case Event.Query:
this.queryBallastState(send);
break;
case Event.SetLevel:
this.setLevel(value || 0, send);
break;
}
// Save current/updated state to the context
this.storeState();
// Save serialized state to the payload
msg.payload = this.serializeState();
delete msg.topic;
send([null, msg]);
if (done)
done();
}
// Clear fade interval when flow has ended in the middle of Fade sequence
onClose() {
this.resetFade();
}
// Method called each time Fade interval is fired. It reacts based on FadeAction on the
// instance. If None - do nothing, else fade a step Up or Down.
// To avoid rounding errors there is a step counter and saved original ballast level on
// the beginning of the fade. Step is calculated as start level +/- levels/step * no. of Steps.
// Ballast level after step can't be higher reps. lower than ballast bounds.
//
// It's important to refresh and store context state because the method is sending
// commands to ballast.
fadeLoopTick() {
if (this.fadeAction === FadeAction.None) {
return;
}
this.refreshState();
const origLevel = this.level;
if (this.fadeAction === FadeAction.Up) {
this.fadeStepCntr += 1;
this.level = Math.min(this.pConf.levelBounds.max, this.fadeStartLevel + Math.round(this.pConf.fadeLevelPerStep * this.fadeStepCntr));
if (origLevel < this.pConf.levelBounds.max) {
this.sendToBallast({
opcode: foxtron_types_1.Opcode.UP,
address: this.pConf.address
});
}
}
if (this.fadeAction === FadeAction.Down) {
this.fadeStepCntr += 1;
this.level = Math.max(this.pConf.levelBounds.min, this.fadeStartLevel - Math.round(this.pConf.fadeLevelPerStep * this.fadeStepCntr));
console.log(`step ${this.fadeStepCntr}, level ${this.level}, orig ${origLevel}`);
console.log(this.pConf.levelBounds);
if (origLevel > this.pConf.levelBounds.min) {
this.sendToBallast({
opcode: foxtron_types_1.Opcode.DOWN,
address: this.pConf.address
});
}
}
this.storeState();
}
// Method to send command to ballast (first output). If send funciton is not supplied
// the message originates here and we are going to send it with node.send method.
sendToBallast(cmd, send) {
if (!send)
send = this.node.send.bind(this.node);
send([{
payload: (0, foxtron_serial_frame_1.foxtronDaliFrame)(cmd)
}, null]);
}
// Method to stop fading action.
// Fade uses Up and Down DALI commands. To sync with calculated level it's forsing it to
// the ballast.
// Method returns true if an actual stop of fade sequence needed to be commenced
stopFade(send) {
if (this.fadeAction === FadeAction.Down || this.fadeAction === FadeAction.Up) {
this.resetFade();
this.setLevel(this.level, send);
return true;
}
return false;
}
// Turn off the ballast
setOff(send) {
this.stopFade(send);
this.sendToBallast({
opcode: foxtron_types_1.Opcode.OFF,
address: this.pConf.address
}, send);
this.isOn = false;
this.level = this.pConf.levelBounds.min;
}
// Set ballast to max level
setMax(send) {
this.stopFade(send);
this.sendToBallast({
opcode: foxtron_types_1.Opcode.RECALL_MAX_LEVEL,
address: this.pConf.address
}, send);
this.isOn = true;
this.level = this.pConf.levelBounds.max;
}
// Set ballast to min level, but keep it turned on
setMin(send) {
this.stopFade(send);
this.sendToBallast({
opcode: foxtron_types_1.Opcode.RECALL_MIN_LEVEL,
address: this.pConf.address
}, send);
this.isOn = true;
this.level = this.pConf.levelBounds.min;
}
// Toggle between max level and off.
// TODO: select between smooth (using Fade Time) and instant toggle
toggle(send) {
this.stopFade(send);
if (!this.isOn || this.level !== this.pConf.levelBounds.max) {
this.setMax(send);
}
else {
this.setOff(send);
}
}
// Start fade Up
startFadeUp(send) {
if (!this.isOn) {
this.setMin(send);
}
this.resetFade(FadeAction.Up, this.level);
}
// Start fade Down
startFadeDown(send) {
if (!this.isOn) {
this.setMax(send);
}
this.resetFade(FadeAction.Down, this.level);
}
// Set ballast to the state stored in context.
// Usefull usually after the flow has begun to sync ballast with node state.
resetBallastState(send) {
console.log(`Reset ballast`);
this.stopFade(send);
if (!this.isOn) {
this.setOff(send);
}
else {
this.setLevel(this.level, send);
}
}
}
// Register constructor function with Node-RED
function main(RED) {
RED.nodes.registerType('foxtron-dali-ballast', function (config) {
new FoxtronDaliBallast(RED, this, config);
});
}