Comparing version 0.0.1 to 0.0.2
var net = require('net'); | ||
function Heatmiser(host, pin, port) { | ||
this.host = host; | ||
this.pin = pin; | ||
this.port = (typeof port === "undefined") ? 8068 : port; | ||
this.model = null; | ||
} | ||
var crc16 = function(buf){ | ||
@@ -28,3 +35,3 @@ // Thanks to http://code.google.com/p/heatmiser-wifi/ for the algorithm | ||
var parse_dcb = function(dcb_buf){ | ||
var model = ['DT', 'DT-E', 'PRT', 'PRT-E', 'PRTHW'][dcb_buf.readUInt8(4)]; | ||
this.model = ['DT', 'DT-E', 'PRT', 'PRT-E', 'PRTHW'][dcb_buf.readUInt8(4)]; | ||
var version = dcb_buf.readUInt8(3); | ||
@@ -97,7 +104,14 @@ var length = dcb_buf.readUInt16LE(0); | ||
var read_device = function(host, port, pin, success, error){ | ||
var client = net.connect({host: host, port: port}, function() { //'connect' listener | ||
var buf = new Buffer([0x93, 0x0B, 0x00, (pin & 0xFF), ((pin & 0xFF00) >> 8), 0x00, 0x00, 0xFF, 0xFF]); | ||
var crc = crc16(buf) | ||
buf = Buffer.concat([buf, (new Buffer([(crc & 0xFF), ((crc & 0xFF00) >> 8)]))]); | ||
// Construct an arbitrary thermostat command | ||
Heatmiser.prototype.command = function(operation, success, error, data) { | ||
len = 7 + data.length; | ||
var buf = new Buffer(5+data.length+2); | ||
buf.writeUInt8(operation, 0); // 0 | ||
buf.writeUInt16LE(len, 1); // 1-2 | ||
buf.writeUInt16LE(this.pin, 3); // 3-4 | ||
data.copy(buf, 5); | ||
var crc = crc16(buf.slice(0,buf.length-2)); | ||
buf.writeUInt16LE(crc, buf.length-2); // last 2 bytes | ||
var client = net.connect({host: this.host, port: this.port}, function() { //'connect' listener | ||
client.write(buf); | ||
@@ -108,3 +122,3 @@ }); | ||
client.on('data', function(data) { | ||
success(parse_response(data)); | ||
success(data); | ||
client.end(); | ||
@@ -114,3 +128,3 @@ }); | ||
client.end(); | ||
error(e); | ||
error((typeof e === 'undefined') ? new Error("Timed out") : e); | ||
}); | ||
@@ -123,3 +137,209 @@ client.on('error', function(e){ | ||
exports.read_device = read_device; | ||
Heatmiser.prototype.read_device = function(success, error){ | ||
var check_response = function(data) { | ||
success(parse_response(data)); | ||
} | ||
this.command(0x93, check_response, error, new Buffer([0x00, 0x00, 0xFF, 0xFF])); | ||
} | ||
Heatmiser.prototype.write_device = function(data, success, error) { | ||
var write = function() { | ||
var items; | ||
try { | ||
items = status_to_dcb(this.model, data); | ||
} catch(e) { | ||
error(e); | ||
return; | ||
} | ||
var buf = Buffer.concat(items); | ||
// First byte to send is the number of items | ||
var buffer = new Buffer(buf.length+1); | ||
buffer[0] = items.length; | ||
buf.copy(buffer, 1); | ||
var check_response = function(data) { | ||
success(parse_response(data)); | ||
} | ||
this.command(0xa3, check_response, error, buffer); | ||
}.bind(this); | ||
// ensure the model is set in the first call to write | ||
if (this.model == null) { | ||
this.read_device(write, error); | ||
} else { | ||
write(); | ||
} | ||
} | ||
var dcb_entry = function(position, data) { | ||
var l = (typeof data === 'number') ? 1 : data.length | ||
var buf = new Buffer(2+1+l); // position, length , data | ||
buf.writeUInt16LE(position, 0); | ||
buf.writeUInt8(l, 2); | ||
if(typeof data === 'number') { | ||
buf.writeUInt8(data, 3); | ||
} | ||
else { | ||
data.copy(buf, 3); | ||
} | ||
return buf; | ||
} | ||
var timeToByteBuffer = function(hours, minutes) { | ||
var buf = new Buffer(2); | ||
buf[0] = hours; | ||
buf[1] = minutes; | ||
return buf; | ||
} | ||
var dateTimeToByteBuffer = function(datetime) { | ||
var buf = new Buffer(7); | ||
var i = 0; | ||
buf[i++] = datetime.getFullYear()-2000; | ||
buf[i++] = datetime.getMonth()+1; | ||
buf[i++] = datetime.getDate(); | ||
var day = datetime.getDay(); | ||
buf[i++] = day == 0 ? 7 : day; // 0 Sunday -> 7 | ||
buf[i++] = datetime.getHours(); | ||
buf[i++] = datetime.getMinutes(); | ||
buf[i++] = datetime.getSeconds(); | ||
return buf; | ||
} | ||
var status_to_dcb = function(model, data) { | ||
var items = []; | ||
for(var key in data) { | ||
switch(key) { | ||
case 'time': | ||
// Current date and time, from a javascript Date object | ||
items.push(dcb_entry(43, dateTimeToByteBuffer(data[key]))); | ||
break; | ||
case 'enabled': | ||
// General operating status (on/off) | ||
items.push(dcb_entry(21, data[key] ? 1 : 0)); | ||
break; | ||
case 'keylock': | ||
// General operating status (on/off) | ||
items.push(dcb_entry(22, data[key] ? 1 : 0)); | ||
break; | ||
case 'holiday': | ||
// holiday mode | ||
// { enabled: false } | ||
// { enabled: true, time: new Date() } | ||
if (('enabled' in data[key]) && !data[key]['enabled']) { | ||
// Cancel holiday mode | ||
items.push(dcb_entry(24, 0)); | ||
} else if ('time' in data[key]) { | ||
// Set return date and time, date without seconds | ||
data[24] = dateTimeToByteBuffer(data[key]['time']).slice(0,5); | ||
} | ||
break; | ||
case 'runmode': | ||
// Run mode (controls heating) | ||
if (model != 'TM1') | ||
items.push(dcb_entry(23, data[key] == 'frost' ? 1 : 0)); | ||
break; | ||
case 'awaymode': | ||
// Away mode (controls hot water) | ||
if (model.match(/(HW|TM1)$/)) | ||
items.push(dcb_entry(31, data[key] == 'away' ? 1 : 0)); | ||
break; | ||
case 'frostprotect': | ||
// Frost protection (temperature only, cannot disable) | ||
if ('target' in data[key]) | ||
items.push(dcb_entry(17, data[key]['target'])); | ||
break; | ||
case 'floorlimit': | ||
// Floor limit (temperature only, cannot disable) | ||
if (model.match(/-E$/) && ('floormax' in data[key])) | ||
items.push(dcb_entry(19, data[key]['floormax'])); | ||
break; | ||
case 'heating': | ||
// Status of heating (target and hold - in minutes - only, cannot turn on/off) | ||
if (model != 'TM1') { | ||
if ('target' in data[key]) | ||
items.push(dcb_entry(18, data[key]['target'])); | ||
if ('hold' in data[key]) | ||
var buf = new Buffer(2); | ||
buf.writeUInt16LE(data[key]['hold'], 0); | ||
items.push(dcb_entry(32, buf)); | ||
} | ||
break; | ||
case 'hotwater': | ||
// Status of hot water (values are different from those read) | ||
if (model.match(/(HW|TM1)$/)) { | ||
if ('boost' in data[key]) | ||
var buf = new Buffer(2); | ||
buf.writeUInt16LE(data[key]['boost'], 0); | ||
items.push(dcb_entry(25, buf)); | ||
if ('on' in data[key]) | ||
items.push(dcb_entry(42, data[key]['on'] ? 1 : 2)); | ||
else | ||
items.push(dcb_entry(42, 0)); | ||
} | ||
break; | ||
case 'comfort': | ||
// Heating comfort levels program | ||
if (model.match(/^PRT/)) { | ||
var days = data[key].length | ||
var days_expected = status.config.progmode == '5/2' ? 2 : 7 | ||
if (days != days_expected) { | ||
throw new Error("Incorrect number of days specified for comfort levels program " + days + ". Expected " + days_expected); | ||
} | ||
for (var day=0;day<days;day++) { | ||
var comfort; | ||
for (var entry=0;entry<4;entry++) { | ||
var row = data[key][day][entry]; | ||
comfort.concat(typeof row !== "undefined" ? timeToByteBuffer(row['time']).push(row['target']) : new Buffer([24,0,16]) ); | ||
var new_schedule = new Buffer([24,0,16]); // default disabled schedule | ||
if (typeof row !== "undefined") { | ||
timeToByteBuffer(row['time']).copy(new_schedule); | ||
new_schedule[2] = row['target']; | ||
} | ||
comfort = Buffer.concat(comfort, new_schedule); | ||
} | ||
items.push(dcb_entry((days == 2 ? 47 : 103) + day*12, comfort)); | ||
} | ||
} | ||
break; | ||
case 'timer': | ||
// Hot water control program | ||
if (model.match(/^(PRTHW|TM1)$/)) { | ||
var days = data[key].length | ||
var days_expected = status.config.progmode == '5/2' ? 2 : 7 | ||
if (days != days_expected) { | ||
throw new Error("Incorrect number of days specified for hot water control program " + days + ". Expected " + days_expected); | ||
} | ||
for (var day=0;day<days;day++) { | ||
var timer; | ||
for (var entry=0;entry<4;entry++) { | ||
var row = data[key][day][entry]; | ||
timer.concat(typeof row !== "undefined" ? timeToByteBuffer(row['on']).concat(timeToByteBuffer(row['off'])) : [24,0,24,0] ); | ||
} | ||
items.push(dcb_entry((days == 2 ? 71 : 187) + day*16, timer)); | ||
} | ||
} | ||
break; | ||
default: | ||
// HERE - Need to add support for item 26 (TM1 countdown) | ||
// Other settings are not writable (including basic configuration) | ||
// Feature 01: $status->{config}->{units} | ||
// Feature 02: $status->{config}->{switchdiff} | ||
// Feature 05: $status->{config}->{outputdelay} | ||
// Feature 06 (06-10): Communications settings | ||
// Feature 07 (11): $status->{config}->{locklimit} | ||
// Feature 08 (12): $status->{config}->{sensor} | ||
// Feature 10 (14): $status->{config}->{optimumstart} | ||
// Feature 12 (16): $status->{config}->{progmode} | ||
throw new Error("Unsupported item for writing: " + key); | ||
} | ||
} | ||
return items; | ||
} | ||
module.exports = Heatmiser; |
{ | ||
"name": "heatmiser", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "A node.js app that talks to heatmiser wifi thermostats", | ||
"main": "lib/heatmiser.js", | ||
"scripts": { | ||
"test": "" | ||
"test": "./node_modules/.bin/mocha" | ||
}, | ||
@@ -22,3 +22,5 @@ "repository": { | ||
"devDependencies": { | ||
"optimist": "*" | ||
"mocha": "*", | ||
"expect.js": "*", | ||
"sinon": "*" | ||
}, | ||
@@ -25,0 +27,0 @@ "engines": { |
@@ -8,1 +8,34 @@ heatmiser-node | ||
https://github.com/bjpirt/heatmiser-js | ||
# Reading the thermostat status | ||
var hm = new Heatmiser('localhost', 1234); | ||
hm.read_device(function(success) { | ||
console.log(success); | ||
}, function(error) { | ||
console.log(error); | ||
}); | ||
# Writing to the thermostat | ||
var log = function(msg) { | ||
console.log(msg); | ||
} | ||
// set the time | ||
hm.write_device({ | ||
time: new Date() | ||
}, log, log); | ||
// set the thermostat to frost mode | ||
hm.write_device({ | ||
runmode: 'frost' | ||
}, log, log); | ||
// set the thermostat hold | ||
hm.write_device({ | ||
heating: { | ||
target: 20, // C | ||
hold: 30 // minutes | ||
} | ||
}, log, log); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
Found 1 instance in 1 package
17634
8
437
2
41
3
3