diff --git a/README.md b/README.md index c695027d..d59bdb13 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. **145-BBB-hardware** - A collection of analogue & digital input & output nodes for the Beaglebone Black diff --git a/hardware/heatmiser-in/100-heatmiser-in.html b/hardware/heatmiser-in/100-heatmiser-in.html new file mode 100644 index 00000000..76b2f73b --- /dev/null +++ b/hardware/heatmiser-in/100-heatmiser-in.html @@ -0,0 +1,62 @@ + + + + + + + diff --git a/hardware/heatmiser-in/100-heatmiser-in.js b/hardware/heatmiser-in/100-heatmiser-in.js new file mode 100644 index 00000000..44eb1396 --- /dev/null +++ b/hardware/heatmiser-in/100-heatmiser-in.js @@ -0,0 +1,191 @@ +/** + * 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 HeatmiserInputNode(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.multiWriteFunc = undefined; + hminnode = this; + this.pollIntervalRef = undefined; + + 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)); + } + hminnode.currentStatus = data.dcb; + if (hminnode.multiWriteFunc) { + hminnode.multiWriteFunc(); + hminnode.multiWriteFunc = undefined; + return; + } + hminnode.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)); + } + hminnode.send(data); + }); + + this.on("close", function() { + if (this.pollIntervalRef) { + clearInterval(this.pollIntervalRef); + this.pollIntervalRef = undefined; + } + }); + + this.read = function() { + if (hminnode.hm) { + hminnode.hm.read_device(); + } + }; + + if (!this.currentStatus) { + this.read(); + this.pollIntervalRef = setInterval(this.read, 30*60*1000); + } + + this.write = function(dcb) { + if (hminnode.hm) { + hminnode.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-in.js] Hit the runmode case"); + } + if (message.payload[key] !== "frost" && message.payload[key] !== "heating") { + util.log("[100-heatmiser-in.js] Warning: Unsupported 'runmode' value passed!"); + return; + } + break; + + // case "holiday" : + // if (DEBUG) { + // util.log("[100-heatmiser-in.js] Hit the holiday case"); + // } + // if (!('enabled' in message.payload[key]) && !('time' in message.payload[key])) { + // util.log("[100-heatmiser-in.js] Warning: Unsupported 'holiday' value passed!"); + // return; + // } + // var time = message.payload[key].time; + // // Ensure hminnode 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-in.js] Hit the hotwater case"); + // } + // if (message.payload[key] !== "on" && message.payload[key] !== "boost" && message.payload[key] !== "off") { + // util.log("[100-heatmiser-in.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-in.js] Hit the heating case"); + } + if (!('target' in message.payload[key]) && !('hold' in message.payload[key])) { + util.log("[100-heatmiser-in.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 hminnode runmode == heating first + if (hminnode.currentStatus.run_mode === "frost_protection") { + // Use the multiWriteFunc as a callback in our success case + hminnode.multiWriteFunc = function() { + hminnode.write(message.payload); + } + hminnode.write({"runmode" : "heating"}); + // End the flow here to ensure no double-writing + return; + } + break; + + default : + break; + } + } + // Valid set of key messages, construct DCB and write + var dcb = message.payload; + if (DEBUG) { + util.log("[100-heatmiser-in.js] Injecting " + JSON.stringify(dcb)); + } + hminnode.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 (message.payload) { + if (typeof(message.payload) === "string") { + message.payload = JSON.parse(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-in.js] Warning: Unsupported key ("+key+") passed!"); + return; + } + } + } + hminnode.validateAndWrite(message); + } + else { + util.log("[100-heatmiser-in.js] Warning: Invalid input passed!"); + return; + } + }); +} +RED.nodes.registerType("heatmiser-in",HeatmiserInputNode); diff --git a/hardware/heatmiser-out/101-heatmiser-out.html b/hardware/heatmiser-out/101-heatmiser-out.html new file mode 100644 index 00000000..9f00948b --- /dev/null +++ b/hardware/heatmiser-out/101-heatmiser-out.html @@ -0,0 +1,62 @@ + + + + + + + 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);