diff --git a/.gitignore b/.gitignore index 47466ca..f272a61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .AppleDouble* +.DS_Store .idea node_modules* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a257ccf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Patrik Mayer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..851069f --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +node-red-contrib-loxone += +This is a work-in-progress node to connect the Loxone Miniserver to +node-red. It uses [node-lox-ws-api](https://github.com/alladdin/node-lox-ws-api) +by Ladislav Dokulil based on Loxone's documenation for the [Websocket API](https://www.loxone.com/dede/wp-content/uploads/sites/2/2016/08/loxone-communicating-with-the-miniserver.pdf). + +The connection is encrypted (hashed) via node-lox-ws-api. + +> Help, pull requests and feedback in general are very welcome! + +I've only tested it with a Temperature Sensor and a Switch module so far as I don't +have an own Loxone-Installation. Gladly a friend of mine lent me his spare miniserver. + +![image of node-red editor](node-red-loxone-editor.png) +![image node-red dashboard](node-red-loxone-dashboard.png) + +Currently working +- +* Configure a miniserver connection +* Loxone-In node +* Load structure file into node-red's editor to choose from +* Select a control and a state to "listen to" which then gets passed to node-red +* Control-name State-Name are given back in the `msg`-object + +You can narrow the result by choosing a room or a category. + +Your structure file can be retrieved via `http:///data/LoxAPP3.json` +An explanation of the file can be found [here](https://www.loxone.com/dede/wp-content/uploads/sites/2/2016/08/loxone-structure-file.pdf) + +Currently partially working, caveats +- +* You have to load the structure file every time you edit a node - should be cached +* The "connected" info under the node in the editor is buggy atm +* Only `controls` are parsed, no `mediaServer`, `weatherServer`, etc. + Is this enough? +* ... + +I've discovered that a switch element emits it's current state (`active`) two times. +One when the trigger-button is pressed and another when the button is released. + +Maybe you can point me out, how to get I1-I8 directly via the WS-API. + +ToDo +- +* Convenience / Testing! +* More info in `msg`-object based on structure file +* Configuration of the encryption mehthod - currently only "Hash" +* Loxone-Out +* better logging, more failsaveness +* ... + +Installation +- +As the node has not reached an initial version it is not published via npm. + +So currently you have to checkout the repository manually, see +https://nodered.org/docs/creating-nodes/packaging#testing-a-node-module-locally + + git clone git@github.com:codmpm/node-red-contrib-loxone.git + cd node-red-contrib-loxone + sudo npm link + cd ~/.node-red + npm link node-red-contrib-loxone + +After that, restart node-red. + +Contributing +- + +1. Fork it! +2. Create your feature branch: git checkout -b my-new-feature +3. Commit your changes: git commit -am 'Add some feature' +4. Push to the branch: git push origin my-new-feature +5. Submit a pull request :D + +Credits +- +Patrik Mayer, 2017 - I'm not affiliated to [Loxone](https://www.loxone.com/) in any way. + +Many thanks to [Nick O'Leary](https://github.com/knolleary), [Dave Conway-Jones](https://github.com/dceejay/) + and everyone else from the node-red Slack-Channel. + +License +- +MIT + diff --git a/loxone/loxone.html b/loxone/loxone.html index f3edc70..3611e71 100644 --- a/loxone/loxone.html +++ b/loxone/loxone.html @@ -1,13 +1,6 @@ @@ -20,8 +13,11 @@ color: '#83B817', defaults: { name: {value: ''}, - miniserver: {type: "loxone-miniserver", required: true}, - stateUUID: {value: '', required: true} + miniserver: {type: 'loxone-miniserver', required: true}, + control: {value: '', required: true}, + controlName: {value: ''}, //TODO: take from structure file + stateName: {value: ''}, //TODO: take from structure file + uuid: {value: '', required: true} }, inputs: 0, outputs: 1, @@ -30,13 +26,15 @@ return this.name || "loxone"; }, oneditprepare: function () { - $('#node-input-loadstruct').on('click', loadStructureFile); - - - + //TODO: check for already loaded structure and select the saved uuid }, oneditsave: function () { + + //not needed it taken from structure file + this.controlName = $('#node-input-control option:selected').text(); + this.stateName = $('#node-input-uuid option:selected').text(); + disableChangeEvents(); }, oneditcancel: function () { @@ -52,12 +50,11 @@ $.getJSON(url).done(function (data) { - console.log(data); + //console.log(data); if (data.state == 'ok') { miniserverStructure = data.structure; - var $roomSelect = $('#node-input-room'); var $categorySelect = $('#node-input-category'); var $controlSelect = $('#node-input-control'); @@ -102,6 +99,7 @@ } $('#node-input-room, #node-input-category').on('change', fillControl); + $controlSelect.on('change', fillState); } $('#return-msg').text(data.msg); @@ -124,7 +122,7 @@ $controlSelect.empty(); - console.log(miniserverStructure); + //console.log(miniserverStructure); var options = []; for (var uuid in miniserverStructure.controls) { @@ -142,7 +140,6 @@ } $controlSelect.append(options.join('')); - $controlSelect.on('change', fillState); if (options.length == 1) { $controlSelect.trigger('change'); } @@ -150,17 +147,20 @@ } function fillState(e) { - console.log('fill state'); - var $stateSelect = $('#node-input-stateUUID'); + var $stateSelect = $('#node-input-uuid'); $stateSelect.empty(); $stateSelect.append(''); if (typeof miniserverStructure.controls[this.value] != 'undefined') { - for (var statename in miniserverStructure.controls[this.value].states) { - if (miniserverStructure.controls[this.value].states.hasOwnProperty(statename)) { - $stateSelect.append(''); + + var control = miniserverStructure.controls[this.value]; + + for (var statename in control.states) { + if (control.states.hasOwnProperty(statename)) { + $stateSelect.append( + ''); } } } @@ -196,7 +196,7 @@ Room - @@ -205,7 +205,7 @@ Category - @@ -214,16 +214,16 @@ Control -
-
@@ -240,15 +240,6 @@ - - - - - - - - - @@ -292,21 +283,6 @@ - - - - - - - - - - - - - - - diff --git a/loxone/loxone.js b/loxone/loxone.js index ce7b952..6d4217d 100644 --- a/loxone/loxone.js +++ b/loxone/loxone.js @@ -49,11 +49,8 @@ module.exports = function (RED) { function _update_event(uuid, evt) { - var data = { - uuid: uuid, - 'event': _limit_string(JSON.stringify(evt), text_logger_limit), - }; - node.log("received update event: " + data.event + ':' + data.uuid); + //node.log("received update event: " + JSON.stringify(evt) + ':' + uid); + node.handleEvent(uuid, evt); } function _limit_string(text, limit) { @@ -63,7 +60,6 @@ module.exports = function (RED) { return text.substr(0, limit) + '...(' + text.length + ')'; } - RED.nodes.createNode(this, config); var node = this; @@ -74,6 +70,7 @@ module.exports = function (RED) { node.rooms = {}; node.categories = {}; node.controls = {}; + node._inputNodes = []; var text_logger_limit = 100; @@ -82,7 +79,7 @@ module.exports = function (RED) { var ws_auth = 'Hash'; - node.log('connecting miniserver...'); + node.log('connecting miniserver at ' + config.host); //TODO: add port to connection var client = new node_lox_ws_api( @@ -96,22 +93,40 @@ module.exports = function (RED) { client.connect(); client.on('connect', function () { - node.log("connected"); + node.log('connected to ' + config.host); node.miniserverConnected = true; }); client.on('authorized', function () { - node.log("authorized"); + node.log('authorized'); node.miniserverAuthenticated = true; node.miniserverConnection = client; + + for (var i = 0; i < node._inputNodes.length; i++) { + node._inputNodes[i].status({ + fill: "green", + shape: "dot", + text: "connected" + }); + } + }); client.on('connect_failed', function () { - node.error("connect failed"); + node.error('connect failed'); + + for (var i = 0; i < node._inputNodes.length; i++) { + node._inputNodes[i].status({ + fill: "red", + shape: "circle", + text: "connection failed" + }); + } + }); client.on('connection_error', function (error) { - node.error("connection error: " + error); + node.error('connection error: ' + error); }); client.on('close', function () { @@ -119,10 +134,19 @@ module.exports = function (RED) { node.miniserverConnected = false; node.miniserverAuthenticated = false; node.miniserverConnection = null; + + for (var i = 0; i < node._inputNodes.length; i++) { + node._inputNodes[i].status({ + fill: "red", + shape: "circle", + text: "connection closed" + }); + } + }); client.on('send', function (message) { - node.log("sent message: " + message); + //node.log("sent message: " + message); }); client.on('message_text', function (message) { @@ -153,23 +177,22 @@ module.exports = function (RED) { }); client.on('message_header', function (header) { - node.log('received message header (' + header.next_state() + '):'); + //node.log('received message header (' + header.next_state() + '):'); //console.log(header); }); client.on('message_event_table_values', function (messages) { - node.log('received value messages:' + messages.length); + //node.log('received value messages:' + messages.length); }); client.on('message_event_table_text', function (messages) { - node.log('received text messages:' + messages.length); + //node.log('received text messages:' + messages.length); }); client.on('get_structure_file', function (data) { node.log("got structure file " + data.lastModified); node.structureData = data; parseStructure(data); - }); client.on('update_event_value', _update_event); @@ -196,12 +219,7 @@ module.exports = function (RED) { for (uuid in data.controls) { if (data.controls.hasOwnProperty(uuid)) { - node.controls[uuid] = { - name: data.controls[uuid].name, - room: data.controls[uuid].room, - cat: data.controls[uuid].cat, - states: data.controls[uuid].states - } + node.controls[uuid] = data.controls[uuid]; } } @@ -217,20 +235,85 @@ module.exports = function (RED) { }); + LoxoneMiniserver.prototype.registerInputNode = function (handler) { + //console.log('registered input node for ' + handler.uuid); + this._inputNodes.push(handler); + }; + + LoxoneMiniserver.prototype.removeInputNode = function (handler) { + this._inputNodes.forEach(function (node, i, inputNodes) { + if (node === handler) { + inputNodes.splice(i, 1); + } + }); + }; + + LoxoneMiniserver.prototype.handleEvent = function (uuid, event) { + + for (var i = 0; i < this._inputNodes.length; i++) { + if (this._inputNodes[i].uuid == uuid) { + + //console.log(this.controls[uuid]); + + this.log('got "' + this._inputNodes[i].stateName + '" for "' + this._inputNodes[i].controlName + '"'); + + var payload; + try { + payload = JSON.parse(event); + } + catch (err) { + payload = event; + } + + var msg = { + payload: payload, + topic: this._inputNodes[i].controlName, + state: this._inputNodes[i].stateName, + //room: this.controls[uuid].room, + //category: this.controls[uuid].cat + + //search states of all controls for our control + //as the msg uuid is not the item uuid + }; + + this._inputNodes[i].send(msg); + + } + } + + }; + + function LoxoneInNode(config) { RED.nodes.createNode(this, config); var node = this; - this.on('input', function (msg) { + if (!config.miniserver || !config.uuid) { + node.status({ + fill: "red", + shape: "ring", + text: "config missing" + }); + return; + } + node.uuid = config.uuid; + node.stateName = config.stateName; //tmp + node.controlName = config.controlName; //tmp - node.send(msg); + node.miniserver = RED.nodes.getNode(config.miniserver); + if (node.miniserver) { + //register node to the desired connection + node.miniserver.registerInputNode(node); + + //this.miniserver.miniserverConnection + //TODO: think about unregistering the node from the connection + } - }); } RED.nodes.registerType("loxone-in", LoxoneInNode); //RED.nodes.registerType("loxone-out", LoxoneOutNode); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/node-red-loxone-dashboard.png b/node-red-loxone-dashboard.png new file mode 100644 index 0000000..a6c634d Binary files /dev/null and b/node-red-loxone-dashboard.png differ diff --git a/node-red-loxone-editor.png b/node-red-loxone-editor.png new file mode 100644 index 0000000..a083d35 Binary files /dev/null and b/node-red-loxone-editor.png differ