From 18b1190029e9b79b36257a5d5d1194078d962b18 Mon Sep 17 00:00:00 2001 From: Sean Bedford Date: Sat, 8 Mar 2014 17:51:40 +0000 Subject: [PATCH 1/6] Added heatmiser integration to allow temperature set and frost modes on heatmiser thermostats --- hardware/heatmiser/100-heatmiser.html | 56 ++++++++ hardware/heatmiser/100-heatmiser.js | 181 ++++++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 hardware/heatmiser/100-heatmiser.html create mode 100644 hardware/heatmiser/100-heatmiser.js diff --git a/hardware/heatmiser/100-heatmiser.html b/hardware/heatmiser/100-heatmiser.html new file mode 100644 index 00000000..77140d7f --- /dev/null +++ b/hardware/heatmiser/100-heatmiser.html @@ -0,0 +1,56 @@ + + + + + + + diff --git a/hardware/heatmiser/100-heatmiser.js b/hardware/heatmiser/100-heatmiser.js new file mode 100644 index 00000000..38fcda15 --- /dev/null +++ b/hardware/heatmiser/100-heatmiser.js @@ -0,0 +1,181 @@ +/** + * Copyright 2013 IBM Corp. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var RED = require(process.env.NODE_RED_HOME+"/red/red"); + +var Heatmiser = require("heatmiser"); +var util = require('util'); + +function HeatmiserNode(n) { + // TODO - holiday and hot water cases when confirmed working + var DEBUG = true; + RED.nodes.createNode(this,n); + this.ip = n.ip || "192.168.0.1"; + this.pin = n.pin || "1234"; + this.multiWriteFunc = undefined; + that = this; + + this.hm = new Heatmiser(this.ip, this.pin); + + this.hm.on('success', function(data) { + if (DEBUG) { + util.log(JSON.stringify(data)); + } + that.currentStatus = data.dcb; + if (that.multiWriteFunc) { + that.multiWriteFunc(); + that.multiWriteFunc = undefined; + return; + } + that.send(data.dcb); + }); + this.hm.on('error', function(data) { + if (DEBUG) { + console.log(JSON.stringify(data)); + } + that.send(data); + }); + + this.read = function() { + that.hm.read_device(); + }; + + if (!this.currentStatus) { + this.read(); + setInterval(this.read, 30000); + } + + this.write = function(dcb) { + that.hm.write_device(dcb); + }; + + this.validateAndWrite = function(message) { + for (var key in message.payload) { + // Ensure our valid keys contain valid values + switch(key) { + case "runmode" : + if (DEBUG) { + util.log("[100-heatmiser.js] Hit the runmode case"); + } + if (message.payload[key] !== "frost" && message.payload[key] !== "heating") { + util.log("[100-heatmiser.js] Warning: Unsupported 'runmode' value passed!"); + return; + } + break; + + // case "holiday" : + // if (DEBUG) { + // util.log("[100-heatmiser.js] Hit the holiday case"); + // } + // if (!('enabled' in message.payload[key]) && !('time' in message.payload[key])) { + // util.log("[100-heatmiser.js] Warning: Unsupported 'holiday' value passed!"); + // return; + // } + // var time = message.payload[key].time; + // // Ensure that time is a date + // if (typeof(time) == "string") { + // util.log("Typeof time was " +typeof(message.payload[key].time)); + // // message.payload[key].time = new Date(message.payload[key].time); + // message.payload[key].time = new Date(2014, 02, 15, 12, 0, 0); + // util.log("Typeof time is now " +typeof(message.payload[key].time)); + // } + // // Also add in away mode (for hot water) if we're on hols + // if (message.payload[key].time) { + // message.payload.away_mode = 1; + // } + // else { + // message.payload.away_mode = 0; + // } + // break; + + // case "hotwater" : + // if (DEBUG) { + // util.log("[100-heatmiser.js] Hit the hotwater case"); + // } + // if (message.payload[key] !== "on" && message.payload[key] !== "boost" && message.payload[key] !== "off") { + // util.log("[100-heatmiser.js] Warning: Unsupported 'hotwater' value passed!"); + // return; + // } + // break; + + case "heating" : + // Ensure heating stays last! It's got a multi write scenario + if (DEBUG) { + util.log("[100-heatmiser.js] Hit the heating case"); + } + if (!('target' in message.payload[key]) && !('hold' in message.payload[key])) { + util.log("[100-heatmiser.js] Warning: Unsupported 'heating' value passed!"); + return; + } + // Set sane temp and time ranges and sanitise to float/int + var target = parseFloat(message.payload[key].target); + var hold = parseInt(message.payload[key].hold); + (target > 30.0) ? message.payload[key].target = 30.0 : message.payload[key].target = target; + (hold > 1440) ? message.payload[key].hold = 1440 : message.payload[key].hold = hold; + (target <= 10.0) ? message.payload[key].target = 10.0 : message.payload[key].target = target; + (hold <= 0) ? message.payload[key].hold = 0 : message.payload[key].hold = hold; + + // Ensure that runmode == heating first + if (that.currentStatus.run_mode === "frost_protection") { + // Use the multiWriteFunc as a callback in our success case + that.multiWriteFunc = function() { + that.write(message.payload); + } + that.write({"runmode" : "heating"}); + // End the flow here to ensure no double-writing + return; + } + break; + + default : + if (DEBUG) { + util.log("[100-heatmiser.js] Hit the default case"); + } + that.read(); + } + } + // Valid set of key messages, construct DCB and write + var dcb = message.payload; + if (DEBUG) { + util.log("[100-heatmiser.js] Injecting " + JSON.stringify(dcb)); + } + that.write(dcb); + }; + + this.on("input", function(message) { + // Valid inputs are heating:{target:, hold:}, read:, runmode:frost/heating, holiday:{enabled:, time:}, hotwater:{'on':1/0 / 'boost':1/0} + if (typeof(message.payload) == "string") { + message.payload = JSON.parse(message.payload); + } + if (message.payload.read) { + that.hm.read_device(); + } + else if (message.payload) { + // Compare message.payload data to confirm valid and send to thermostat + var validInputs = ["heating", "runmode"]; + for (var key in message.payload) { + if (message.payload.hasOwnProperty(key)) { + if (validInputs.indexOf(key) < 0) { + util.log("[100-heatmiser.js] Warning: Unsupported key ("+key+") passed!"); + return; + } + } + } + that.validateAndWrite(message); + } + }); +} +RED.nodes.registerType("heatmiser",HeatmiserNode); From 8a60d1f9185a7a26635c453186f473121dd12bd2 Mon Sep 17 00:00:00 2001 From: henols Date: Wed, 19 Mar 2014 22:20:27 +0100 Subject: [PATCH 2/6] Support for csv payload and manipulation insertion time --- io/emoncms/88-emoncms.html | 38 ++++++++++++++++++++++++++++++++------ io/emoncms/88-emoncms.js | 18 ++++++++++++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/io/emoncms/88-emoncms.html b/io/emoncms/88-emoncms.html index 8f20f07e..d210d22d 100644 --- a/io/emoncms/88-emoncms.html +++ b/io/emoncms/88-emoncms.html @@ -20,7 +20,14 @@
- + + +
+
+
@@ -31,14 +38,21 @@
-
Topic is not mandatory, if Topic is left blank msg.topic will used. Topic overrides msg.topic
- Node Group (numeric) is not mandatory, if Node Group is left blank msg.nodegrpup will used. Node Group overrides msg.nodegroup
+
If Payload is set to csv, msg.payload can be a comma separated values and the key will + be automatically generated by Emoncms, if Payload is set to json, msg.payload can only be a single value and + the key value can be manually or programmatically set.

+ Key is not mandatory, if Key is left blank msg.topic will used. Key overrides msg.topic

+ Node (numeric) is not mandatory, if Node is left blank msg.nodegrpup will used. Node over rides msg.nodegroup. +
@@ -85,7 +112,6 @@ category: 'config', defaults: { server: {value:"http://localhost",required:true}, - // apikey: {value:"",required:true}, name: {value:""} }, label: function() { diff --git a/io/emoncms/88-emoncms.js b/io/emoncms/88-emoncms.js index 4138b0bc..7635cb34 100644 --- a/io/emoncms/88-emoncms.js +++ b/io/emoncms/88-emoncms.js @@ -20,6 +20,7 @@ function EmoncmsServerNode(n) { RED.nodes.createNode(this,n); this.server = n.server; this.name = n.name; + this.payloadType = n.payloadType; var credentials = RED.nodes.getCredentials(n.id); if (credentials) { this.apikey = credentials.apikey; @@ -71,18 +72,27 @@ function Emoncms(n) { this.baseurl = sc.server; this.apikey = sc.apikey; + this.payloadType = n.payloadType; this.topic = n.topic ||""; this.nodegroup = n.nodegroup || ""; var node = this; if (this.baseurl.substring(0,5) === "https") { var http = require("https"); } else { var http = require("http"); } this.on("input", function(msg) { - - var topic = this.topic || msg.topic; + this.url = this.baseurl + '/input/post.json?'; + if(this.payloadType == 'json'){ + var topic = this.topic || msg.topic; + this.url += 'json={' + topic + ':' + msg.payload+'}'; + } else { + this.url += 'csv='+msg.payload; + } + this.url += '&apikey='+this.apikey; var nodegroup = this.nodegroup || msg.nodegroup; - this.url = this.baseurl + '/input/post.json?json={' + topic + ':' + msg.payload+'}&apikey='+this.apikey; if(nodegroup != ""){ - this.url += '&node='+nodegroup; + this.url += '&node=' + nodegroup; + } + if(typeof msg.time !== 'undefined'){ + this.url += '&time=' + msg.time; } node.log("[emoncms] "+this.url); http.get(this.url, function(res) { From e6f90f9b8d3021885fe48f0e125e3abcca6bcaf5 Mon Sep 17 00:00:00 2001 From: henols Date: Thu, 20 Mar 2014 14:51:16 +0100 Subject: [PATCH 3/6] Deals with csv and json payload in a smarter way and manipulation insertion time --- io/emoncms/88-emoncms.html | 36 +++++++++--------------------------- io/emoncms/88-emoncms.js | 10 ++++------ 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/io/emoncms/88-emoncms.html b/io/emoncms/88-emoncms.html index d210d22d..de9234d5 100644 --- a/io/emoncms/88-emoncms.html +++ b/io/emoncms/88-emoncms.html @@ -20,37 +20,31 @@
- - -
-
- +
-
If Payload is set to csv, msg.payload can be a comma separated values and the key will - be automatically generated by Emoncms, if Payload is set to json, msg.payload can only be a single value and - the key value can be manually or programmatically set.

- Key is not mandatory, if Key is left blank msg.topic will used. Key overrides msg.topic

+
If msg.payload holds comma separated values (csv), Key and msg.topic is ignored and the key will + be automatically generated by Emoncms. If msg.payload holds a single value and Key and msg.topic is not specified, msg.payload + will be treated as a csv otherwise it will be treated as a json payload and the key value can be manually or programmatically set.

+ Key is not mandatory, if Key is left blank msg.topic will used. Key overrides msg.topic, it will be ignored if msg.payload is holding csv

Node (numeric) is not mandatory, if Node is left blank msg.nodegrpup will used. Node over rides msg.nodegroup.
@@ -75,18 +69,6 @@ }, labelStyle: function() { return this.name?"node_label_italic":""; - }, - oneditprepare: function() { - $("#node-input-payloadType").change(function() { - var id = $("#node-input-payloadType option:selected").val(); - if (id == "json") { - $("#node-input-row-topic").show(); - } else { - $("#node-input-row-topic").hide(); - } - }); - $("#node-input-payloadType").val(this.payloadType); - $("#node-input-payloadType").change(); } }); diff --git a/io/emoncms/88-emoncms.js b/io/emoncms/88-emoncms.js index 7635cb34..938cdad5 100644 --- a/io/emoncms/88-emoncms.js +++ b/io/emoncms/88-emoncms.js @@ -20,7 +20,6 @@ function EmoncmsServerNode(n) { RED.nodes.createNode(this,n); this.server = n.server; this.name = n.name; - this.payloadType = n.payloadType; var credentials = RED.nodes.getCredentials(n.id); if (credentials) { this.apikey = credentials.apikey; @@ -72,7 +71,6 @@ function Emoncms(n) { this.baseurl = sc.server; this.apikey = sc.apikey; - this.payloadType = n.payloadType; this.topic = n.topic ||""; this.nodegroup = n.nodegroup || ""; var node = this; @@ -80,11 +78,11 @@ function Emoncms(n) { else { var http = require("http"); } this.on("input", function(msg) { this.url = this.baseurl + '/input/post.json?'; - if(this.payloadType == 'json'){ - var topic = this.topic || msg.topic; - this.url += 'json={' + topic + ':' + msg.payload+'}'; - } else { + var topic = this.topic || msg.topic; + if(msg.payload.indexOf(',') > -1 || topic.trim() == ''){ this.url += 'csv='+msg.payload; + } else { + this.url += 'json={' + topic + ':' + msg.payload+'}'; } this.url += '&apikey='+this.apikey; var nodegroup = this.nodegroup || msg.nodegroup; From c4ec78e8545869e3fb21d87829f013b4dd706acb Mon Sep 17 00:00:00 2001 From: Sean Bedford Date: Mon, 24 Mar 2014 20:15:20 +0000 Subject: [PATCH 4/6] Fix input node (+2 squashed commits) Squashed commits: [3079c2d] Added Heatmiser input and output nodes [62bd1f3] Added Heatmiser input and output nodes Fix input node bugs --- README.md | 4 + .../100-heatmiser-in.html} | 20 ++-- .../100-heatmiser-in.js} | 102 ++++++++++-------- hardware/heatmiser-out/101-heatmiser-out.html | 62 +++++++++++ hardware/heatmiser-out/101-heatmiser-out.js | 91 ++++++++++++++++ 5 files changed, 226 insertions(+), 53 deletions(-) rename hardware/{heatmiser/100-heatmiser.html => heatmiser-in/100-heatmiser-in.html} (70%) rename hardware/{heatmiser/100-heatmiser.js => heatmiser-in/100-heatmiser-in.js} (62%) create mode 100644 hardware/heatmiser-out/101-heatmiser-out.html create mode 100644 hardware/heatmiser-out/101-heatmiser-out.js diff --git a/README.md b/README.md index fa93915e..78dadddd 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,10 @@ Copyright 2013 IBM Corp. under [the Apache 2.0 license](LICENSE). **79-sensorTag** - Reads data from the Ti BLE SensorTag device. +**100-heatmiser-in** - Writes settings for temperature and frost protection to Heatmiser thermostats. + +**101-heatmiser-out** - Reads settings from Heatmiser thermostats at a polling interval. + **101-scanBLE** - Scans for a particular Bluetooth Low Energy (BLE) device. ### IO diff --git a/hardware/heatmiser/100-heatmiser.html b/hardware/heatmiser-in/100-heatmiser-in.html similarity index 70% rename from hardware/heatmiser/100-heatmiser.html rename to hardware/heatmiser-in/100-heatmiser-in.html index 77140d7f..76b2f73b 100644 --- a/hardware/heatmiser/100-heatmiser.html +++ b/hardware/heatmiser-in/100-heatmiser-in.html @@ -1,5 +1,5 @@ - - + + + + diff --git a/hardware/heatmiser-out/101-heatmiser-out.js b/hardware/heatmiser-out/101-heatmiser-out.js new file mode 100644 index 00000000..a7ec6b07 --- /dev/null +++ b/hardware/heatmiser-out/101-heatmiser-out.js @@ -0,0 +1,91 @@ +/** + * Copyright 2014 Sean Bedford + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +var RED = require(process.env.NODE_RED_HOME+"/red/red"); + +var Heatmiser = require("heatmiser"); +var util = require('util'); + +function HeatmiserOutputNode(n) { + // TODO - holiday and hot water cases when confirmed working + var DEBUG = false; + RED.nodes.createNode(this,n); + this.ip = n.ip || "192.168.0.1"; + this.pin = n.pin || "1234"; + this.pollTime = n.pollTime*60*1000 || 30*60*1000; + this.pollIntervalRef = undefined; + hmoutnode = this; + + this.hm = new Heatmiser(this.ip, this.pin); + + this.hm.on('success', function(data) { + if (DEBUG) { + util.log("[100-heatmiser-in.js] - Successfully wrote data. Response : " + JSON.stringify(data)); + } + hmoutnode.send({topic: "", payload:JSON.stringify(data.dcb)}); + }); + this.hm.on('error', function(data) { + if (DEBUG) { + util.log("[100-heatmiser-in.js] - Error during data setting : " + JSON.stringify(data)); + } + hmoutnode.send(data); + }); + + this.read = function() { + if (hmoutnode.hm) { + hmoutnode.hm.read_device(); + } + }; + + if (!this.currentStatus) { + this.read(); + this.pollIntervalRef = setInterval(this.read, this.pollTime); + } + + this.on("close", function() { + if (this.pollIntervalRef) { + clearInterval(this.pollIntervalRef); + this.pollIntervalRef = undefined; + } + }); + + this.on("input", function(message) { + // Valid inputs are heating:{target:, hold:}, read:, runmode:frost/heating, holiday:{enabled:, time:}, hotwater:{'on':1/0 / 'boost':1/0} + if (message.payload == "undefined" || !message.payload) { + message.payload = {read : true}; + } + if (typeof(message.payload) == "string") { + message.payload = JSON.parse(message.payload); + } + if (message.payload.read) { + hmoutnode.read(); + } + else if (message.payload) { + // Compare message.payload data to confirm valid and send to thermostat + var validInputs = ["heating", "runmode"]; + for (var key in message.payload) { + if (message.payload.hasOwnProperty(key)) { + if (validInputs.indexOf(key) < 0) { + util.log("[100-heatmiser.js] Warning: Unsupported key ("+key+") passed!"); + return; + } + } + } + hmoutnode.validateAndWrite(message); + } + }); +} +RED.nodes.registerType("heatmiser-out",HeatmiserOutputNode); From 9808e8a3a6c01828e854240409be4efe969c3c89 Mon Sep 17 00:00:00 2001 From: henols Date: Thu, 3 Apr 2014 09:42:49 +0200 Subject: [PATCH 5/6] Automatic detection of csv and json call and simplified help text --- io/emoncms/88-emoncms.html | 21 +++------------------ io/emoncms/88-emoncms.js | 8 +++----- 2 files changed, 6 insertions(+), 23 deletions(-) diff --git a/io/emoncms/88-emoncms.html b/io/emoncms/88-emoncms.html index de9234d5..22cd2e27 100644 --- a/io/emoncms/88-emoncms.html +++ b/io/emoncms/88-emoncms.html @@ -19,10 +19,6 @@
-
- - -
@@ -31,22 +27,13 @@
-
If msg.payload holds comma separated values (csv), Key and msg.topic is ignored and the key will - be automatically generated by Emoncms. If msg.payload holds a single value and Key and msg.topic is not specified, msg.payload - will be treated as a csv otherwise it will be treated as a json payload and the key value can be manually or programmatically set.

- Key is not mandatory, if Key is left blank msg.topic will used. Key overrides msg.topic, it will be ignored if msg.payload is holding csv

- Node (numeric) is not mandatory, if Node is left blank msg.nodegrpup will used. Node over rides msg.nodegroup. -
diff --git a/hardware/hue/104-hue_manage.js b/hardware/hue/104-hue_manage.js index e01f4be5..9d1374f2 100644 --- a/hardware/hue/104-hue_manage.js +++ b/hardware/hue/104-hue_manage.js @@ -92,27 +92,30 @@ function HueNode(n) { if(node.lamp_status=="AUTO") { var color; var brightness; - //check for lamp ID in the topic - if(myMsg.topic.length>1) { - var tmp_status = myMsg.topic.split(":"); - myMsg.topic = tmp_status[1]; - lamp = tmp_status[0]; + + //get lamp id from msg.lamp: + lamp = myMsg.lamp; + + //get brightness: + brightness = myMsg.brightness; + + //get colour either from msg.color or msg.topic + if(myMsg.color!=null && myMsg.color.length>0) { + color = myMsg.color; + } + else if(myMsg.topic!=null && myMsg.topic.length>0) { + color = myMsg.topic; } - //check for brightness & color: - if(myMsg.payload.length>1) { - var tmp_topic = myMsg.payload.split(":"); - color = tmp_topic[0]; - brightness = tmp_topic[1]; - } + //check the payload for on/off/alert: //case of ALERT: - if(myMsg.topic=="ALERT"){ + if(myMsg.payload=="ALERT" || myMsg.payload=="alert"){ api.setLightState(lamp, state.alert()).then(displayResult).fail(displayError).done(); } //case of ON: - if(myMsg.topic=="ON") { + if(myMsg.payload=="ON" || myMsg.payload=="on") { api.setLightState(lamp, state.on().rgb(hexToRgb(color).r,hexToRgb(color).g,hexToRgb(color).b).brightness(brightness)).then(displayResult).fail(displayError).done(); } else {