From 18b1190029e9b79b36257a5d5d1194078d962b18 Mon Sep 17 00:00:00 2001 From: Sean Bedford Date: Sat, 8 Mar 2014 17:51:40 +0000 Subject: [PATCH 1/2] 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 c4ec78e8545869e3fb21d87829f013b4dd706acb Mon Sep 17 00:00:00 2001 From: Sean Bedford Date: Mon, 24 Mar 2014 20:15:20 +0000 Subject: [PATCH 2/2] 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);