node-red-contrib-myhome-bticino-v2
Version:
Control Bticino / Legrand MyHome components from Node-RED
209 lines (195 loc) • 10.8 kB
JavaScript
/*jshint esversion: 7, strict: implied, node: true */
module.exports = function (RED) {
let mhutils = require ('./myhome-utils');
function MyHomeEventSessionNode (config) {
RED.nodes.createNode (this, config);
var node = this;
let gateway = RED.nodes.getNode (config.gateway);
let runningMonitor = new mhutils.eventsMonitor (gateway);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Add listener on node linked to a dedicated function call to be able to remove it on close
const listenerFunction = function (ownFamilyName , frame) {
// MSG1 : add frame received from BUS
let msg = {payload: frame};
// MSG1 : add major node configuration info
msg.mh_nodeConfigInfo = {
'name' : config.name ,
'ownFamilyName' : ownFamilyName ,
'gateway' : {
'name' : gateway.name ,
'host' : gateway.host ,
'port' : gateway.port
}
};
// MSG2 : build secondary output (detailled in 'universal mode')
let msg2 = node.processReceivedBUSFrames (msg, ownFamilyName, frame);
// Send both outputs
node.send ([msg, msg2]);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Add all listerners on gateway based on types we have to monitor
// Define the function which is to be called on any triggered command received from the gateway
// LIGHTS
if (config.own_lights) {
runningMonitor.addMonitoredEvent ('OWN_LIGHTS', listenerFunction);
}
// SHUTTERS
if (config.own_shutters) {
runningMonitor.addMonitoredEvent ('OWN_SHUTTERS', listenerFunction);
}
// TEMPERATURE MANAGEMENT
if (config.own_temperature) {
runningMonitor.addMonitoredEvent ('OWN_TEMPERATURE', listenerFunction);
}
// SCENARIO MANAGEMENT
if (config.own_scenario) {
runningMonitor.addMonitoredEvent ('OWN_SCENARIO', listenerFunction);
}
// ENERGY MANAGEMENT
if (config.own_energy) {
runningMonitor.addMonitoredEvent ('OWN_ENERGY', listenerFunction);
}
// OTHERS
if (config.own_others) {
runningMonitor.addMonitoredEvent ('OWN_OTHERS', listenerFunction);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Function called when a MyHome BUS frame is received ///////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
this.processReceivedBUSFrames = function (mainMsg, ownFamilyName, frame) {
// Init new msg (cloned from main) & add basinc info on payload (common to all family types)
let msg = RED.util.cloneMessage (mainMsg);
let payload = msg.payload = {};
payload.ownFamilyName = ownFamilyName;
payload.command_received = frame;
// Append content specifc per type
let frameMatch = null;
switch (ownFamilyName) {
case 'OWN_LIGHTS':
// Checks 1 : Light point/group update [*1*<status>|<dimmerLevel10>*where##]
// - <status> [0-1] : 0 = OFF / 1 = ON
// - <dimmerLevel10> [2-10] : 2 = 20% / 3 = 30% / ... / 9 = 90% / 10 = 100%
// Note : the RegEx must only keep 2 characters when <status> begins with 1 to skip 30 or 31 since these are dimming UP / DOWN
frameMatch = frame.match (/^\*1\*(\d|1\d)\*(#{0,1}\d{1,4})(?:#4#(\d\d)){0,1}##/);
// frameMatch[1] = state (0 = off, 1 = on, 2 for 20%, 3 for 30%... 10 for 100%)
// frameMatch[2] = lightgroupid (begins with # if is a group)
// frameMatch[3] = bus level (01 to 15)
if (frameMatch !== null) {
payload.buslevel = (frameMatch[3] === undefined) ? 'private_riser' : frameMatch[3];
payload.state = (frameMatch[1] === '0') ? 'OFF' : 'ON';
payload.brightness = (frameMatch[1] === '1') ? 100 : (parseInt(frameMatch[1]) * 10);
payload.lightid = frameMatch[2].replace('#' , '');
payload.isgroup = (frameMatch[2][0] === '#');
}
// Checks 2 : Light point/group dimmer info update [*#1*<where>*1*<dimmerLevel100>*<dimmerSpeed>##]
// - <dimmerLevel100> [100-200] : 100 = off / 200 = Max
if (frameMatch === null) {
frameMatch = frame.match (/^\*#1\*(#{0,1}\d{1,4})(?:#4#(\d\d)){0,1}\*1\*(\d+)\*\d+##/);
// frameMatch[1] = lightgroupid (begins with # if is a group)
// frameMatch[2] = bus level (01 to 15)
// frameMatch[3] = dimmer level (100 = OFF to 200 = 100%)
if (frameMatch !== null) {
payload.buslevel = (frameMatch[2] === undefined) ? 'private_riser' : frameMatch[2];
payload.state = (frameMatch[3] === '100') ? 'OFF' : 'ON';
payload.brightness = (parseInt(frameMatch[3]) - 100);
payload.lightid = frameMatch[1].replace('#' , '');
payload.isgroup = (frameMatch[1][0] === '#');
}
}
break;
case 'OWN_SHUTTERS':
// Checks 1 : Shutter point/group update [*2*<status>*where##]
// - <status> [0-2] : 0 = Stop / 1 = Up / 2 = Down
frameMatch = frame.match (/^\*2\*(\d+)\*(#{0,1}\d{1,4})(?:#4#(\d\d)){0,1}##/);
// frameMatch[1] = <status> [0-2] : 0 = Stop / 1 = Up / 2 = Down
// frameMatch[2] = shuttergroupid (begins with # if is a group)
// frameMatch[3] = bus level (01 to 15)
if (frameMatch !== null) {
payload.buslevel = (frameMatch[3] === undefined) ? 'private_riser' : frameMatch[3];
let actionDescr_Shutter = {'0':'STOP', '1':'OPEN', '2':'CLOSE'}[frameMatch[1]];
payload.state = (actionDescr_Shutter === undefined) ? '?' : actionDescr_Shutter;
payload.shutterid = frameMatch[2].replace('#' , '');
payload.isgroup = (frameMatch[2][0] === '#');
}
break;
case 'OWN_TEMPERATURE':
// Checks 1 : current temperature and set objective frames (OpenWebNet doc) :
// - current temperature (master probe) : *#4*where*0*T## (or *#4*where*0*T*3## if local offset included)
// The T field is composed from 4 digits c1c2c3c4, included between “0020” (2°temperature) and “0430” (43°temperature).
// c1 is always equal to 0, it indicates a positive temperature. The c2c3 couple indicates the temperature values between [02° - 43°].
frameMatch = frame.match (/^\*#4\*(\d{1,2})\*0\*0(\d{3})(?:\*3|)##/);
// frameMatch[1] = <WHERE> [1-99] : zone ID
// frameMatch[2] = T temperature (193 = 19.3°)
if (frameMatch !== null) {
payload.zoneid = frameMatch[1];
payload.state = parseInt (frameMatch[2]) / 10;
}
break;
case 'OWN_SCENARIO':
// Checks 1 : Basic scenario (CEN) [*15*WHAT(#<ACTION_TYPE>)*WHERE##]
// - WHAT = push button N value [00-31]
// - <ACTION_TYPE> = 1: Release after short pressure (<0.5s) / 2: Release after an extended pressure (>= 0.5s) / 3: Extended pressure (sent every 0.5s as long as button is pressed)
// - WHERE = push button virtual address (A/PL)
frameMatch = frame.match (/^\*15\*(\d+)#{0,1}(\d|)\*(\d{2,4})(?:#4#(\d\d)){0,1}##/);
// frameMatch[1] = push button N value [00-31]
// frameMatch[2] = <ACTION_TYPE>
// frameMatch[3] = WHERE = push button virtual address (A/PL)
// frameMatch[4] = bus level (01 to 15)
if (frameMatch !== null) {
payload.buslevel = (frameMatch[4] === undefined) ? 'private_riser' : frameMatch[4];
payload.scenariotype = 'CEN';
payload.scenarioid = frameMatch[3];
payload.buttonID = frameMatch[1];
let actionDescr_CEN = {'1':'SHORT', '2':'LONG', '3':'LONG_ONGOING'}[frameMatch[2]];
payload.actionType = (actionDescr_CEN === undefined) ? 'PRESS_START' : actionDescr_CEN;
}
// Checks 2 : Advanced scenario (CEN+) [*25*<ACTION_TYPE>#WHAT*WHERE##]
// - <ACTION_TYPE> = 21: Short pressure (<0.5s) / 22: Start of extended pressure (>= 0.5s) / 23: Extended pressure (sent every 500ms) / 24: Release after an extended pressure
// - WHAT = push button N value [0-31]
// - WHERE = 2+[0-2047] Virtual Address
if (frameMatch === null) {
frameMatch = frame.match (/^\*25\*(\d{2})#(\d+)\*2(\d{1,4})(?:#4#(\d\d)){0,1}##/);
// frameMatch[1] = <ACTION_TYPE>
// frameMatch[2] = WHAT : push button N value [0-31]
// frameMatch[3] = WHERE : [0-2047] Virtual Address
// frameMatch[4] = bus level (01 to 15)
if (frameMatch !== null) {
payload.buslevel = (frameMatch[4] === undefined) ? 'private_riser' : frameMatch[4];
payload.scenariotype = 'CEN+';
payload.scenarioid = frameMatch[3];
payload.buttonID = frameMatch[2];
let actionDescr_CEN = {'21':'SHORT', '22':'LONG_START', '23':'LONG_ONGOING', '24':'LONG'}[frameMatch[1]];
payload.actionType = (actionDescr_CEN === undefined) ? '?' : actionDescr_CEN;
}
}
break;
case 'OWN_ENERGY':
// Check 1 [WHAT=113] : Current power consumption (Instant, in Watts) [*#18*<Where>*113*<Val>##] with <Val> = WATT)
frameMatch = frame.match (/^\*#18\*(5|7)(\d{1,3})(?:#0){0,1}\*113\*(\d+)##/);
// frameMatch[1] = WHERE type (5 for meter, 7 for actuator)
// frameMatch[2] = WHERE id : N value [1-255]
// frameMatch[3] = <Val> = WATT
if (frameMatch !== null) {
payload.metertype = (frameMatch[1] === '5') ? 'meter' : 'actuator';
payload.meterscope = 'instant';
payload.meterid = frameMatch[2];
payload.metered_Power = parseInt(frameMatch[3]);
}
break;
case 'OWN_OTHERS':
// Nothing managed in 'universal mode' yet
break;
}
// Checks : all done, if one match was successful, it means a 'universal mode' msg was build, return it
if (frameMatch !== null) {
return msg;
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
node.on ('close', function(done) {
runningMonitor.clearAllMonitoredEvents ();
done();
});
}
RED.nodes.registerType ("myhome-eventsession", MyHomeEventSessionNode);
};