node-red-contrib-myhome-bticino-v2
Version:
Control Bticino / Legrand MyHome components from Node-RED
206 lines (189 loc) • 10.5 kB
JavaScript
/*jshint esversion: 7, strict: implied, node: true */
module.exports = function (RED) {
let mhutils = require ('./myhome-utils');
function MyHomeShutterNode (config) {
RED.nodes.createNode (this, config);
var node = this;
let gateway = RED.nodes.getNode (config.gateway);
let runningMonitor = new mhutils.eventsMonitor (gateway);
// Build the shutter point name. If node is configured as being a group, add '#' as prefix
// When NOT working on private riser, include the bus level (with suffix '#4#xx' where xx is the BUS id)
let buslevel = config.buslevel || 'private_riser';
node.shuttergroupid = ((config.isgroup) ? '#' : '') + config.shutterid + ((buslevel === 'private_riser') ? '' : '#4#' + buslevel);
// All current zone received values stored in memory from the moment node is loaded
let payloadInfo = node.payloadInfo = {};
payloadInfo.state = '?';
node.lastPayloadInfo = JSON.stringify (payloadInfo); // SmartFilter : kept in memory to be able to compare whether an update occurred while processing msg
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Register node for updates
node.on ('input', function (msg) {
node.processInput (msg);
});
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Add listener on node linked to a dedicated function call to be able to remove it on close
const listenerFunction = function (ownFamilyName , frame) {
let msg = {};
node.processReceivedBUSFrames (msg, frame);
};
runningMonitor.addMonitoredEvent ('OWN_SHUTTERS', listenerFunction);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Function called when a MyHome BUS frame is received ///////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
this.processReceivedBUSFrames = function (msg, frame) {
if (typeof (msg.payload) === 'undefined') {
msg.payload = {};
}
let payload = msg.payload;
// When the msg contains a topic with specified 'state/', it means function was called internally (from 'processInput') to refresh values.
// In this case, even if no frame is found to update something, node is refreshed and msg are sent
let forceRefreshAndMsg = (msg.topic === 'state/' + config.topic);
// Check whether received command is linked to current configured shutter point
let processedFrames = 0;
for (let curFrame of (typeof(frame) === 'string') ? [frame] : frame) {
let frameMatch;
// Checks 1 : Shutter point/group update [*2*<status>*where##]
// - <status> [0-2] : 0 = Stop / 1 = Up / 2 = Down
frameMatch = curFrame.match ('^\\*2\\*(\\d+)\\*(' + node.shuttergroupid + '|0)##');
if (frameMatch !== null) {
switch (frameMatch[1]) {
case '0':
// STOP
payloadInfo.state = 'STOP';
node.status ({fill: 'grey', shape: (config.isgroup) ? 'ring' : 'dot', text: ((config.isgroup) ? RED._('mh-shutter.node.status-isgroup') : '') + RED._('mh-shutter.node.status-stopped')});
break;
case '1':
// OPEN
payloadInfo.state = 'OPEN';
node.status ({fill: 'yellow', shape: (config.isgroup) ? 'ring' : 'dot', text: ((config.isgroup) ? RED._('mh-shutter.node.status-isgroup') : '') + RED._('mh-shutter.node.status-opened')});
break;
case '2':
// CLOSE
payloadInfo.state = 'CLOSE';
node.status ({fill: 'grey', shape: (config.isgroup) ? 'ring' : 'dot', text: ((config.isgroup) ? RED._('mh-shutter.node.status-isgroup') : '') + RED._('mh-shutter.node.status-closed')});
break;
}
}
// If we reached here with a non null match, it means command was useful for node
if (frameMatch !== null) {
processedFrames++;
}
}
// Checks : all done, if nothing was processed, abord (no node / flow update detected), excepted when refresh is 'forced'
if (processedFrames === 0 && !forceRefreshAndMsg) {
return;
}
// Send msg back as new flow : only send update as new flow when something changed after having received this new BUS info
// (but always send it when SmartFilter is disabled or when running in 'state/' mode, i.e. read-only mode)
if (!config.skipevents || forceRefreshAndMsg) {
let newPayloadinfo = JSON.stringify (payloadInfo);
if (!config.smartfilter || newPayloadinfo !== node.lastPayloadInfo || forceRefreshAndMsg) {
// MSG1 : Build primary msg
// MSG1 : Received command info : only include source command when was provided as string (when is an array, it comes from .processInput redirected here)
if (!Array.isArray(frame)) {
payload.command_received = frame;
}
// MSG1 : add major node configuration info on both returned message
msg.mh_nodeConfigInfo = {
'name' : config.name ,
'topic' : config.topic ,
'buslevel' : config.buslevel ,
'shutterid' : config.shutterid ,
'isgroup' : config.isgroup ,
'gateway' : {
'name' : gateway.name ,
'host' : gateway.host ,
'port' : gateway.port
}
};
// MSG1 : Add all current node stored values to payload
payload.state = payloadInfo.state;
// MSG1 : Add misc info
msg.topic = 'state/' + config.topic;
// MSG2 : Build secondary payload
let msg2 = mhutils.buildSecondaryOutput (RED.util.cloneMessage (msg), payloadInfo, config, 'On', 'OPEN', 'CLOSE');
// Store last sent payload info & send both msg to output1 and output2
node.lastPayloadInfo = newPayloadinfo;
node.send ([msg, msg2]);
}
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Function called when a message (payload) is received from the node-RED flow ///////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
this.processInput = function (msg) {
if (typeof(msg) === 'string') {
try {msg = JSON.parse(msg);} catch(error){}
}
// Only process input received from flow when the topic matches with configuration of nodes
let isReadOnly = config.isstatusrequest || false;
if (msg.topic === 'state/' + config.topic) {
// Running in 'state/' mode, force read-only regardless of node config mode
isReadOnly = true;
} else if (msg.topic !== 'cmd/' + config.topic) {
return;
}
// Get payload and apply conversions (asked state can be set in 'msg.payload',
// 'msg.payload.state' or 'msg.payload.On', value being either true/false or OPEN/CLOSE/STOP
// Final result is always kept in 'msg.payload.state' = 'OPEN', 'CLOSE' or 'STOP'
if (msg.payload === undefined) {
msg.payload = {};
} else if (typeof(msg.payload) === 'string') {
try {msg.payload = JSON.parse(msg.payload);} catch(error){}
}
if (typeof(msg.payload) === 'object') {
if (msg.payload.state === undefined && msg.payload.On !== undefined) {
msg.payload.state = (msg.payload.On) ? 'OPEN' : 'CLOSE';
}
} else if (typeof(msg.payload) === 'boolean') {
msg.payload = {'state': (msg.payload) ? 'OPEN' : 'CLOSE'};
} else if (typeof(msg.payload) === 'string') {
msg.payload = {'state': msg.payload};
}
let payload = msg.payload;
// Build the OpenWebNet command to be sent
let commands = [];
if (!isReadOnly) {
if (payload.state === 'OPEN') {
commands.push ('*2*1*' + node.shuttergroupid + '##');
} else if (payload.state === 'CLOSE') {
commands.push ('*2*2*' + node.shuttergroupid + '##');
} else if (payload.state === 'STOP') {
commands.push ('*2*0*' + node.shuttergroupid + '##');
}
}
if ((isReadOnly || commands.length) && !config.isgroup) {
// In Read-Only mode : build a status enquiry request (no status update sent)
// In Write mode : Since the gateway does not 'respond' when changing point state, we also add a second call to ask for point status after update.
// Note : This does not work for groups
commands.push ('*#2*' + node.shuttergroupid + '##');
}
if (commands.length === 0) {
return;
}
// Send the command on the BUS through the MyHome gateway
mhutils.executeCommand (node, commands, gateway, 0, false,
function (commands, cmd_responses, cmd_failed) {
// Build main payload to return payloads to outputs
payload.command_sent = commands; // Include initial SCS/BUS message which was sent in main payload
payload.command_responses = cmd_responses; // include the BUS responses when emitted command provides a result (can hold multiple values)
if (cmd_failed.length) {
// Also add failed requests, but only if some failed
payload.command_failed = cmd_failed;
}
// Once commands were sent, call internal function to force node info refresh (using 'state/') and msg outputs
msg.topic = 'state/' + config.topic;
node.processReceivedBUSFrames (msg, cmd_responses);
}, function (cmd_failed, nodeStatusErrorMsg) {
// Error, only update node state
node.status ({fill: 'red', shape: 'dot', text: nodeStatusErrorMsg});
});
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
node.on ('close', function(done) {
// If any listener was defined, removed it now otherwise node will remain active in memory and keep responding to Gateway incoming calls
runningMonitor.clearAllMonitoredEvents ();
done();
});
}
RED.nodes.registerType('myhome-shutter', MyHomeShutterNode);
};