diff --git a/app/components/gui/mqttForm.js b/app/components/gui/mqttForm.js index 9453c00..c66e30a 100644 --- a/app/components/gui/mqttForm.js +++ b/app/components/gui/mqttForm.js @@ -1,21 +1,23 @@ "use strict"; var React = require('react'); - +var Grid = require('react-bootstrap').Grid; var Col = require('react-bootstrap').Col; var Row = require('react-bootstrap').Row; var Modal = require('react-bootstrap').Modal; var Input = require('react-bootstrap').Input; var Button = require('react-bootstrap').Button; -var LinkedStateMixin = require('react-addons-linked-state-mixin'); - +var Form = require('react-bootstrap').Form; +var FormControl = require('react-bootstrap').FormControl; +var FormGroup = require('react-bootstrap').FormGroup; +var ControlLabel = require('react-bootstrap').ControlLabel; +var Radio = require('react-bootstrap').Radio; var Switch = require('react-bootstrap-switch'); var Blink1SerialOption = require('./blink1SerialOption'); var MqttForm = React.createClass({ - mixins: [LinkedStateMixin], propTypes: { rule: React.PropTypes.object.isRequired, allowMultiBlink1: React.PropTypes.bool, @@ -26,20 +28,17 @@ var MqttForm = React.createClass({ onCopy: React.PropTypes.func }, getInitialState: function() { - return { - // name: rule.name, - // patternId: rule.patternId - }; + return {}; }, - // FIXME: why am I doing this? + componentWillReceiveProps: function(nextProps) { var rule = nextProps.rule; this.setState({ type: 'mqtt', - enabled: rule.enabled, - name: rule.name, - actionType: 'play-pattern', - patternId: rule.patternId || nextProps.patterns[0].id, + enabled: rule.enabled || false, + name: rule.name || 'new rule', + actionType: rule.actionType || 'parse-color', + //patternId: rule.patternId || nextProps.patterns[0].id, blink1Id: rule.blink1Id || "0", topic: rule.topic || "", url: rule.url || "", @@ -53,6 +52,16 @@ var MqttForm = React.createClass({ handleBlink1SerialChange: function(blink1Id) { this.setState({blink1Id: blink1Id}); }, + handleActionType: function(e) { + var actionType = e.target.value; + this.setState({actionType:actionType}); + }, + handleInputChange: function(event) { + var target = event.target; + var value = target.type === 'checkbox' ? target.checked : target.value; + var name = target.name; + this.setState({ [name]: value }); + }, render: function() { var self = this; @@ -63,38 +72,85 @@ var MqttForm = React.createClass({ return (
- + MQTT Settings

{this.state.errormsg}

-
- - - - - - - {this.props.patterns.map( createPatternOption, this )} - + + + Rule Name + + + + + + + MQTT Topic + + + + + + + MQTT URL + + + + + + + MQTT username + + + + + + + MQTT password + + + + + + + + + + + + Parse output as JSON (color or pattern) + + + Parse output as pattern name + + + Parse output as color + + + + + {!this.props.allowMultiBlink1 ? null : } - +
diff --git a/app/components/gui/toolTable.js b/app/components/gui/toolTable.js index 861d1ac..e017d45 100644 --- a/app/components/gui/toolTable.js +++ b/app/components/gui/toolTable.js @@ -30,7 +30,7 @@ var MailForm = require('./mailForm'); var ScriptForm = require('./scriptForm'); var SkypeForm = require('./skypeForm'); var TimeForm = require('./timeForm'); -// var MqttForm = require('./mqttForm'); +var MqttForm = require('./mqttForm'); var ToolTableList = require('./toolTableList'); @@ -105,7 +105,7 @@ var ToolTable = React.createClass({ TimeService.reloadConfig(); } else if( rule.type === 'mqtt' ) { - // MqttService.reloadConfig(); + MqttService.reloadConfig(); } }, handleSaveForm: function(data) { @@ -193,12 +193,6 @@ var ToolTable = React.createClass({ } } - // - return (
@@ -220,6 +214,12 @@ var ToolTable = React.createClass({ onSave={this.handleSaveForm} onCancel={this.handleCancelForm} onDelete={this.handleDeleteRule} onCopy={this.handleCopyRule} /> + + Add Script Add URL Add File + Add MQTT Add Skype Add Alarm diff --git a/app/maingui.js b/app/maingui.js index 27d9528..69ed79d 100644 --- a/app/maingui.js +++ b/app/maingui.js @@ -79,7 +79,7 @@ var MailService = require('./server/mailService'); var SkypeService = require('./server/skypeService'); var ScriptService = require('./server/scriptService'); var TimeService = require('./server/timeService'); -// var MqttService = require('./server/mqttService'); +var MqttService = require('./server/mqttService'); setTimeout( function() { log.msg("services starting..."); @@ -88,7 +88,7 @@ setTimeout( function() { SkypeService.start(); ScriptService.start(); TimeService.start(); - // MqttService.start(); + MqttService.start(); log.msg("services started"); }, 2000); diff --git a/app/package.json b/app/package.json index 1a76ef3..2ca3f5b 100644 --- a/app/package.json +++ b/app/package.json @@ -22,6 +22,7 @@ "imap": "^0.8.19", "is-electron-renderer": "^2.0.1", "moment": "^2.17.1", + "mqtt": "^4.3.7", "nconf": "^0.11.4", "needle": "^1.5.1", "node-blink1": "^0.5.1", diff --git a/app/server/mqttService.js b/app/server/mqttService.js index 2eb14da..62621fa 100644 --- a/app/server/mqttService.js +++ b/app/server/mqttService.js @@ -2,14 +2,15 @@ 'use strict'; +var mqtt = require('mqtt'); +var tinycolor = require('tinycolor2'); var conf = require('../configuration'); +var utils = require('../utils'); var log = require('../logger'); var Eventer = require('../eventer'); - -// var mqtt = require('mqtt'); -// var mqtt = require('../mqtt.min'); +var PatternsService = require('./patternsService'); var MqttService = { config: {}, @@ -24,56 +25,194 @@ var MqttService = { start: function() { var self = this; - this.config = conf.readSettings('eventServices:mqttService'); + self.config = conf.readSettings('eventServices:mqttService'); if( !this.config ) { log.msg("MqttService.reloadConfig: NO CONFIG"); - this.config = { + self.config = { type: 'mqtt', service: 'mqttService', enabled: true, + reconnectPeriod: 10000, }; - conf.saveSettings('eventServices:mqttService', this.config); + conf.saveSettings('eventServices:mqttService', self.config); } // var allrules = conf.readSettings('eventRules') || []; var allrules = conf.readSettings('eventRules') || []; - this.rules = allrules.filter( function(r){return r.type === 'mqtt';} ); + self.rules = allrules.filter( function(r){return r.type === 'mqtt';} ); - self.rules.forEach( function(rule) { + self.rules.map( function(rule) { log.msg("MqttService.start: rule:", rule); + if( !rule.enabled ) { return; } + var pass = ''; + try { + if( rule.passwordHash !== '' ) { // allow password-less login + pass = utils.decrypt( rule.passwordHash ); + } + } catch(err) { + log.msg('MqttService: ERROR bad password for username', rule.username); + } // FIXME: impelement sanity checks // if( !rule.url ) { } // if !rule.topic ) { } - var config = { - // there will be more here? + var mqtt_config = { + reconnectPeriod: self.config.reconnectPeriod }; - if( rule.username || rule.password ) { - config.username = rule.username; - config.password = rule.password; - } - var client = client = mqtt.connect( rule.url, config ); + mqtt_config.username = rule.username; + mqtt_config.password = pass; + log.msg("MqttService.start: mqtt_config:", mqtt_config); + var client = mqtt.connect( rule.url, mqtt_config ); + self.errorLogged = false; // reset client.on('connect', function () { + log.msg("MqttService.connected"); + Eventer.addStatus( {type:'info', source:rule.type, id:rule.name, text:"connected"} ); client.subscribe( rule.topic ); }); - client.on('error', (error) => { - console.log('MqttService Errored', error); + client.on('disconnect', function() { + log.msg("MqttService.disconnect"); + }); + client.on('close', function() { + log.msg("MqttService.close"); + if( !self.errorLogged ) { + Eventer.addStatus( {type:'info', source:rule.type, id:rule.name, text:"connection closed, bad auth?"} ); + } + }); + client.on('end', function() { + log.msg("MqttService.end"); + }); + client.on('error', function(error) { + console.log("bAKKBKBKB",error); + log.msg('MqttService.error: error json',JSON.stringify(error), error.toString()); + Eventer.addStatus( {type:'info', source:rule.type, id:rule.name, text:error.toString() } ); + self.errorLogged = true; }); client.on('message', function (topic, message) { - // message is Buffer - Eventer.addStatus( {type:'info', source:'mqtt', id:rule.name, text:message.toString()} ); - console.log("topic:", topic, "message:",message.toString()) + self.parse(rule, message.toString()); // message is Buffer, thus .toString() + // Eventer.addStatus( {type:'trigger', source:rule.type, id:rule.name, text:message.toString()} ); + Eventer.addStatus( {type:'info', source:rule.type, id:rule.name, text:message.toString()} ); + log.msg("MqttService: message: topic:", topic, "message:",message.toString()); }); rule.client = client; }); }, stop: function() { - this.rules.forEach( function(rule) { - if( rule.client ) { - rule.client.end(); - rule.client = null; + log.msg("MqttService.stop"); + this.rules.forEach( function(rule) { + if( rule.client ) { + rule.client.end(); + rule.client = null; + } + }); + }, + + playPattern: function(pattid,ruleid,blink1id) { + if( PatternsService.playPatternFrom( ruleid, pattid, blink1id ) ) { + // this.lastPatterns[ruleid] = pattid; + return pattid; + } + return false; + }, + + /** + * Parse the output from a MQTT response. + * Plays patterns if match. + * Sends log messages with source & id of rule. + * Checks for the following content: + * if 'actionType == 'parse-json', treat content as JSON, + * and look for 'pattern' or 'color' keys + * 'pattern' can be meta-pattern like: '~off' and '~blink' + * if 'actionType == 'parse-pattern', attempt to find a pattern + * with the "pattern:" format, and play it. + * Can also use meta-patterns here. + * if 'actionType == 'parse-color', look in text for RGB hex color string + * + * @param {Rule} rule eventRules rule for this content + * @param {String} str the content to be parsed, potentially multiple lines + * @return {[type]} [description] + */ + parse: function(rule, str) { + if( typeof str != "string" ) { + str = (str) ? str.toString() : ''; // convert to string + } + str = str.substring(0,this.config.maxStringLength); + var self = this; + //var patternre = /pattern:\s*(#*\w+)/; // orig + //var patternre = /pattern:\s*(\"([^"])*\"|#?\w+)/; // suggested by @slakichi in issue #101 + var patternre = /pattern:\s*("*)(.+)\1/; // match everything either quoted or not + var colorre = /(#[0-9a-f]{6}|#[0-9a-f]{3}|color:\s*(.+?)\s)/i; // regex to match hex color codes or 'color:' names + var matches; + + // if( self.lastEvents[rule.name] === str && rule.actOnNew ) { + // Eventer.addStatus( {type:'info', source:rule.type, id:rule.name, text:'not modified'}); + // return; + // } + // self.lastEvents[rule.name] = str; + // Eventer.addStatus( { type:'trigger', text:data.substring(0,40), source:rule.type, id:rule.name} ); + + var actionType = rule.actionType; + if( actionType === 'parse-json' ) { + var json = null; + try { + json = JSON.parse(str); + if( json.pattern ) { + // returns true on found pattern // FIXME: go back to using 'findPattern' + if( this.playPattern( json.pattern, rule.name, rule.blink1Id ) ) { + Eventer.addStatus( {type:'trigger', source:rule.type, id:rule.name, text:json.pattern}); + } + else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'no pattern '+json.pattern}); + } + } + else if( json.color ) { + var c = tinycolor(json.color); + if( c.isValid() ) { + Eventer.addStatus( {type:'trigger', source:rule.type, id:rule.name, text:json.color}); + this.playPattern( c.toHexString(), rule.name, rule.blink1Id ); + } else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'invalid color '+json.color}); + } + } + } catch(error) { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:error.message}); } - }); + } + else if( actionType === 'parse-pattern' ) { + matches = patternre.exec( str ); + if( matches ) { + var patt_name = matches[2]; // it's always the 2nd match, either quoted or not + + if( this.playPattern( patt_name, rule.name, rule.blink1Id ) ) { + Eventer.addStatus( {type:'trigger', source:rule.type, id:rule.name, text:patt_name}); + } + else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'no pattern '+str}); + } + } + else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'no pattern '+str}); + } + } + else { // parse-color + matches = colorre.exec(str); + if( matches && matches) { + var colormatch = matches[2]; + if( !colormatch ) { colormatch = matches[1]; } + + var color = tinycolor( colormatch ); + if( color.isValid() ) { + Eventer.addStatus( {type:'trigger', source:rule.type, id:rule.name, text:colormatch}); + this.playPattern( color.toHexString(), rule.name, rule.blink1Id ); + } + else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'invalid color '+colormatch}); + } + } + else { + Eventer.addStatus( {type:'error', source:rule.type, id:rule.name, text:'no color found in:'+str}); + } + } } + }; module.exports = MqttService;