UNPKG

node-red-contrib-myhome-bticino-v2

Version:
361 lines (342 loc) 19.2 kB
/*jshint esversion: 7, strict: implied, loopfunc: true, node: true */ module.exports = function (RED) { let mhutils = require ('./myhome-utils'); const LONGPRESS_ONGOING_INTERVAL = 400; function MyHomeScenarioNode (config) { RED.nodes.createNode (this, config); var node = this; let gateway = RED.nodes.getNode (config.gateway); let runningMonitor = new mhutils.eventsMonitor (gateway); // 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.scenarioid = config.scenarioid + ((buslevel === 'private_riser') ? '' : '#4#' + buslevel); // All current zone received values stored in memory from the moment node is loaded let payloadInfo = node.payloadInfo = {}; 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_SCENARIO', 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 light point let multiOutput = []; let processedFrames = 0; for (let curFrame of (typeof(frame) === 'string') ? [frame] : frame) { let curButtonID = ''; let curActionType = ''; let nodeStatusIcon = ['grey','ring']; let nodeStatusText = ''; let frameMatch; let curButtonLastState; // 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) if (config.scenariotype === 'CEN') { frameMatch = curFrame.match ('^\\*15\\*(\\d+)#{0,1}(\\d|)\\*' + node.scenarioid + '##'); if (frameMatch !== null) { curButtonID = frameMatch[1]; switch (frameMatch[2]) { case '1': // Release after short pressure (<0.5s) curActionType = 'SHORT'; break; case '2': // Release after an extended pressure (>= 0.5s) curActionType = 'LONG'; break; case '3': // Extended pressure (sent every 0.5s as long as button is pressed) curActionType = 'LONG_ONGOING'; break; default: // no param provided, means initial pressure curActionType = 'PRESS_START'; } } } // 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 (config.scenariotype === 'CEN+') { frameMatch = curFrame.match ('^\\*25\\*(\\d{2})#(\\d+)\\*2' + node.scenarioid + '##'); if (frameMatch !== null) { curButtonID = frameMatch[2]; switch (frameMatch[1]) { case '21': // Short pressure (<0.5s) curActionType = 'SHORT'; break; case '22': // Start of extended pressure (>= 0.5s) curActionType = 'LONG_START'; break; case '23': // Extended pressure (sent every 0.5s as long as button is pressed) curActionType = 'LONG_ONGOING'; break; case '24': // Release after an extended pressure curActionType = 'LONG'; break; } } } // If we reached here with a non null match, it means command was useful for node if (frameMatch !== null) { processedFrames++; // Define action info based on type curButtonLastState = payloadInfo['buttonsLastState_' + curButtonID] || {}; switch (curActionType) { case 'PRESS_START': // Start of (short or extended) pressure curButtonLastState = {}; curButtonLastState.actionStart = Date.now(); nodeStatusText = RED._('mh-scenario.node.status-action-startpress'); break; case 'LONG_START': // Start of extended pressure (>= 0.5s) curButtonLastState = {}; curButtonLastState.actionStart = Date.now(); nodeStatusText = RED._('mh-scenario.node.status-action-startpress-long'); break; case 'LONG_ONGOING': // Extended pressure (sent every 0.5s as long as button is pressed) curButtonLastState.actionEnd = Date.now(); curButtonLastState.countExtPressures = (curButtonLastState.countExtPressures||0) + 1; nodeStatusText = RED._('mh-scenario.node.status-action-longpress-ongoing'); break; case 'SHORT': // Short pressure (<0.5s) curButtonLastState = {}; curButtonLastState.actionStart = Date.now(); curButtonLastState.actionEnd = Date.now(); nodeStatusText = RED._('mh-scenario.node.status-action-endpress-short'); break; case 'LONG': // Release after an extended pressure curButtonLastState.actionEnd = Date.now(); nodeStatusText = RED._('mh-scenario.node.status-action-endpress-long'); break; } // Append info which are common to all states curButtonLastState.buttonID = parseInt(curButtonID); curButtonLastState.actionType = curActionType; curButtonLastState.actionDuration = (curButtonLastState.actionEnd - curButtonLastState.actionStart) || 0; curButtonLastState.state = curButtonID + ':' + curButtonLastState.actionType + ((curButtonLastState.actionDuration > 0) ? ':' + curButtonLastState.actionDuration.toString() : ''); nodeStatusText = RED._('mh-scenario.node.status-action-button') + curButtonLastState.buttonID + ': '+ nodeStatusText + ((curButtonLastState.actionDuration > 0) ? ' (' + (curButtonLastState.actionDuration/1000).toFixed(1).toString() + 's)': ''); payloadInfo['buttonsLastState_' + curButtonID] = curButtonLastState; // Transfer all current button info to main payload (as direct properties of it) Object.getOwnPropertyNames(curButtonLastState).forEach (function(objectName) { payload[objectName] = curButtonLastState[objectName]; }); // See if such action is monitored for a defined rule to be sent to a secondary output for (let i = 0; i < config.rules.length; i++) { multiOutput[i+1] = null; let curRule = config.rules[i]; // Check whether pressed button is within a range this rule must monitor if (curButtonLastState.buttonID < parseInt(curRule.buttonFrom) || curButtonLastState.buttonID > parseInt(curRule.buttonTo)) { continue; } // Check whether action is a monitored one let toMonitor = false; if (curButtonLastState.actionType == 'SHORT' && curRule.onEndShortPress) { toMonitor = true; } else if ((curButtonLastState.actionType == 'LONG_START' || curButtonLastState.actionType == 'PRESS_START') && curRule.onStartPress) { toMonitor = true; } else if (curButtonLastState.actionType == 'LONG_ONGOING' && curRule.onDuringLongPress) { toMonitor = true; } else if (curButtonLastState.actionType == 'LONG' && curRule.onEndLongPress) { if (curButtonLastState.actionDuration >= curRule.minLongPressDuration) { toMonitor = true; } } // Reached here : monitored action, build payload (which is actually simply the current button state with all collected info) if (toMonitor) { let curMsg = multiOutput[i+1] = {}; curMsg.payload = curButtonLastState; nodeStatusIcon[0] = 'green'; } } // Update Node displayed status node.status ({fill: nodeStatusIcon[0], shape: nodeStatusIcon[1], text: nodeStatusText}); } } // 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 & spread it to all other multi msg built msg.mh_nodeConfigInfo = { 'name' : config.name , 'topic' : config.topic , 'buslevel' : config.buslevel , 'scenariotype' : config.scenariotype , 'scenarioid' : config.scenarioid , 'gateway' : { 'name' : gateway.name , 'host' : gateway.host , 'port' : gateway.port } }; multiOutput.forEach(function(multiMsg) { if (multiMsg !== null) { multiMsg.mh_nodeConfigInfo = JSON.parse(JSON.stringify(msg.mh_nodeConfigInfo)); } }); // MSG1 : Add all current node stored values to payload Object.getOwnPropertyNames(payloadInfo).forEach (function(objectName) { if (objectName.match('buttonsLastState_\\d+')) { payload[objectName] = payloadInfo[objectName]; } }); // MSG1 : Add misc info and add primary output to outputs msg.topic = 'state/' + config.topic; multiOutput[0] = msg; // Store last sent payload info & send both msg to output1 and output2 node.lastPayloadInfo = newPayloadinfo; node.send(multiOutput); } } }; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // 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' or 'msg.payload.state' 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) === 'string' || typeof(msg.payload) === 'number') { msg.payload = {'state': msg.payload.toString()}; } // Get values of buttonID and duration from payload text to object (assumig structure is 'ButtonID:Duration') if (typeof(msg.payload.state) === 'string' && msg.payload.buttonID === undefined && msg.payload.actionDuration === undefined) { let stateValue = msg.payload.state.split(/:|,|;/); msg.payload.buttonID = stateValue[0]; msg.payload.actionDuration = parseInt(stateValue[1]) || 0; } let payload = msg.payload; let commands = []; let commandsDelay = 0; if (!isReadOnly) { // Ensure data is valid to build command if (isNaN (payload.buttonID) || parseInt(payload.buttonID) < 0 || parseInt(payload.buttonID) > 31) { node.warn ('Button ID (' + payload.buttonID + ') is invalid or not in allowed [0-31] range.'); return; } // Define all based commands which can be used let baseCommands = {}; if (config.scenariotype === 'CEN') { // 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) baseCommands.SHORT = '*15*WHAT*WHERE##;*15*WHAT#1*WHERE##'; // must send 2 commands (START and direct STOP) baseCommands.LONG_START = '*15*WHAT*WHERE##'; baseCommands.LONG_ONGOING = '*15*WHAT#3*WHERE##'; baseCommands.LONG_END = '*15*WHAT#2*WHERE##'; payload.buttonID = ('0' + payload.buttonID).slice(-2); // for CEN commands, the WHAT is always 2 digits } else if (config.scenariotype === 'CEN+') { // 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 baseCommands.SHORT = '*25*21#WHAT*2WHERE##'; baseCommands.LONG_START = '*25*22#WHAT*2WHERE##'; baseCommands.LONG_ONGOING = '*25*23#WHAT*2WHERE##'; baseCommands.LONG_END = '*25*24#WHAT*2WHERE##'; } // Based on duration asked (it defaults to 0), we have to send 1 or multiple commands let actionDuration = parseInt(payload.actionDuration) || 0; if (actionDuration < 500) { // Command must be sent in short press mode commands.push (baseCommands.SHORT); } else { // Command must be sent in long press mode (we need to have a repeat command sent at least every 500ms) commandsDelay = LONGPRESS_ONGOING_INTERVAL; commands.push (baseCommands.LONG_START); while (actionDuration > commandsDelay) { commands.push (baseCommands.LONG_ONGOING); actionDuration = actionDuration - (commandsDelay + 50); // 50ms is added since it is +/- a normal delay required for each command to be technically sent } commands.push (baseCommands.LONG_END); } // Replace all the 'WHAT' & 'WHERE' by the current button ID & Scenario ID for (let i = 0; i < commands.length; i++) { commands[i] = commands[i].replace (/WHAT/g, payload.buttonID).replace (/WHERE/g, node.scenarioid); } } // when no command to send (and not working in read only mode where we only send current node info), abord if (commands.length === 0 & !isReadOnly) { return; } // Send the command on the BUS through the MyHome gateway mhutils.executeCommand (node, commands, gateway, commandsDelay, 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-scenario', MyHomeScenarioNode); };