From d47fd19d4be34c5cc3b218721048b0cbbdd6e2a1 Mon Sep 17 00:00:00 2001 From: Robin Bolscher Date: Wed, 14 Oct 2015 23:11:34 +0200 Subject: [PATCH] Implemented network search functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If a new neo instance is initialized without a host specified, it will start scanning the network for matching devices. If no host specified, the ‘ready’ event will be called on your neo instance to let you know it is done searching. --- examples/example-neo.js | 112 +++---- examples/example-wifi.js | 40 +-- lib/heatmiser.js | 6 +- lib/neo.js | 258 +++++++-------- lib/wifi.js | 676 ++++++++++++++++++++------------------- package.json | 2 +- test/wifi.js | 274 ++++++++-------- 7 files changed, 689 insertions(+), 679 deletions(-) diff --git a/examples/example-neo.js b/examples/example-neo.js index dbcc458..3f9d5b2 100644 --- a/examples/example-neo.js +++ b/examples/example-neo.js @@ -1,61 +1,63 @@ -var heatmiser = require("../lib/heatmiser"); - -var neo = new heatmiser.Neo("192.168.1.112"); - -neo.on('success', function(data) { - console.log(data); -}); -neo.on('error', function(data) { - console.log(data); -}); - -neo.info(); -neo.statistics(); - -var devices = ['bathroom', 'livingroom']; - -neo.setAway(false, devices); -neo.setStandby(false, devices); - -var comfortLevels = { - "bathroom": { - "monday": { - "wake": ["07:00", 20], - "leave": ["09:00", 16], - "return": ["24:00", 21], - "sleep": ["24:00", 16] - }, - "sunday": { - "wake": ["09:00", 20], - "leave": ["11:00", 16], - "return": ["24:00", 21], - "sleep": ["24:00", 16] - } - }, - "livingroom": { - "monday": { - "wake": ["07:00", 19], - "leave": ["08:30", 16], - "return": ["16:30", 19], - "sleep": ["23:00", 16] +var heatmiser = require( "../lib/heatmiser" ); + +var neo = new heatmiser.Neo(); +neo.on( 'ready', function () { + + neo.on( 'success', function ( data ) { + console.log( data ); + } ); + neo.on( 'error', function ( data ) { + console.log( data ); + } ); + + neo.info(); + neo.statistics(); + + var devices = [ 'bathroom', 'livingroom' ]; + + neo.setAway( false, devices ); + neo.setStandby( false, devices ); + + var comfortLevels = { + "bathroom": { + "monday": { + "wake": [ "07:00", 20 ], + "leave": [ "09:00", 16 ], + "return": [ "24:00", 21 ], + "sleep": [ "24:00", 16 ] + }, + "sunday": { + "wake": [ "09:00", 20 ], + "leave": [ "11:00", 16 ], + "return": [ "24:00", 21 ], + "sleep": [ "24:00", 16 ] + } }, - "sunday": { - "wake": ["09:00", 19], - "leave": ["10:00", 16], - "return": ["20:00", 19], - "sleep": ["23:00", 16] + "livingroom": { + "monday": { + "wake": [ "07:00", 19 ], + "leave": [ "08:30", 16 ], + "return": [ "16:30", 19 ], + "sleep": [ "23:00", 16 ] + }, + "sunday": { + "wake": [ "09:00", 19 ], + "leave": [ "10:00", 16 ], + "return": [ "20:00", 19 ], + "sleep": [ "23:00", 16 ] + } } } -} -var keys = Object.keys(comfortLevels); -for (var i=0; i> 12) ^ nibble]; - } +var crc16 = function ( buf ) { + // Thanks to http://code.google.com/p/heatmiser-wifi/ for the algorithm + // Process 4 bits of data + var crc16_4bits = function ( crc, nibble ) { + var lookup = [ 0x0000, 0x1021, 0x2042, 0x3063, + 0x4084, 0x50A5, 0x60C6, 0x70E7, + 0x8108, 0x9129, 0xA14A, 0xB16B, + 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF ]; + return ((crc << 4) & 0xffff) ^ lookup[ (crc >> 12) ^ nibble ]; + } - // Process the whole message - var crc = 0xffff; - for(var i=0; i> 4); - crc = crc16_4bits(crc, buf[i] & 0x0f); - } + // Process the whole message + var crc = 0xffff; + for ( var i = 0; i < buf.length; i++ ) { + crc = crc16_4bits( crc, buf[ i ] >> 4 ); + crc = crc16_4bits( crc, buf[ i ] & 0x0f ); + } - // Return the CRC - return crc; + // Return the CRC + return crc; } +var parse_dcb = function ( dcb_buf ) { + var model = [ 'DT', 'DT-E', 'PRT', 'PRT-E', 'PRTHW' ][ dcb_buf.readUInt8( 4 ) ]; + var version = dcb_buf.readUInt8( 3 ); + var length = dcb_buf.readUInt16LE( 0 ); + if ( length != dcb_buf.length ) throw "Incorrect DCB length"; + if ( model !== 'PRTHW' ) version &= 0x7F; + var program_mode = [ '5/2', '7' ][ dcb_buf.readUInt8( 16 ) ]; - -var parse_dcb = function(dcb_buf){ - var model = ['DT', 'DT-E', 'PRT', 'PRT-E', 'PRTHW'][dcb_buf.readUInt8(4)]; - var version = dcb_buf.readUInt8(3); - var length = dcb_buf.readUInt16LE(0); - if(length != dcb_buf.length) throw "Incorrect DCB length"; - if(model !== 'PRTHW') version &= 0x7F; - var program_mode = ['5/2', '7'][dcb_buf.readUInt8(16)]; - - return { - length: length, - vendor_id: ['HEATMISER', 'OEM'][dcb_buf.readUInt8(2)], - model: model, - version: version/10, - temp_format: ['C', 'F'][dcb_buf.readUInt8(5)], - switch_differential: dcb_buf.readUInt8(6)/2, - frost_protection: !!dcb_buf.readUInt8(7), - calibration_offset: dcb_buf.readUInt16LE(8), - output_delay: dcb_buf.readUInt8(10), - up_down_key_limit: dcb_buf.readUInt8(11), - sensor_selection: ['built_in_only', 'remote_only', 'floor_only', 'built_in+floor', 'remote+floor'][dcb_buf.readUInt8(13)], - optimum_start: dcb_buf.readUInt8(14), - rate_of_change: dcb_buf.readUInt8(15), - program_mode: program_mode, - frost_protect_temp: dcb_buf.readUInt8(17), - set_room_temp: dcb_buf.readUInt8(18), - floor_max_limit: dcb_buf.readUInt8(19), - floor_max_limit_enabled: !!dcb_buf.readUInt8(20), - device_on: !!dcb_buf.readUInt8(21), - key_lock: !!dcb_buf.readUInt8(22), - run_mode: ['heating', 'frost_protection'][dcb_buf.readUInt8(23)], - away_mode: !!dcb_buf.readUInt8(24), - holiday_enabled: !!dcb_buf.readUInt8(30), - holiday_return_date: {}, - temp_hold_minutes: dcb_buf.readUInt16LE(31), - remote_air_temp: dcb_buf.readUInt16LE(33) === 0xFFFF ? null : dcb_buf.readUInt16LE(33)/10, - floor_temp: dcb_buf.readUInt16LE(35) === 0xFFFF ? null : dcb_buf.readUInt16LE(35)/10, - built_in_air_temp: dcb_buf.readUInt16LE(37) === 0xFFFF ? null : dcb_buf.readUInt16LE(37)/10, - error_code: dcb_buf.readUInt8(39), - heating_on: !!dcb_buf.readUInt8(40), - boost_in_min: dcb_buf.readUInt16LE(41), - hot_water_on: !!((model == 'PRTHW') ? dcb_buf.readUInt8(43) : false), - current_time: extract_date(dcb_buf, model), - } + return { + length: length, + vendor_id: [ 'HEATMISER', 'OEM' ][ dcb_buf.readUInt8( 2 ) ], + model: model, + version: version / 10, + temp_format: [ 'C', 'F' ][ dcb_buf.readUInt8( 5 ) ], + switch_differential: dcb_buf.readUInt8( 6 ) / 2, + frost_protection: !!dcb_buf.readUInt8( 7 ), + calibration_offset: dcb_buf.readUInt16LE( 8 ), + output_delay: dcb_buf.readUInt8( 10 ), + up_down_key_limit: dcb_buf.readUInt8( 11 ), + sensor_selection: [ 'built_in_only', 'remote_only', 'floor_only', 'built_in+floor', 'remote+floor' ][ dcb_buf.readUInt8( 13 ) ], + optimum_start: dcb_buf.readUInt8( 14 ), + rate_of_change: dcb_buf.readUInt8( 15 ), + program_mode: program_mode, + frost_protect_temp: dcb_buf.readUInt8( 17 ), + set_room_temp: dcb_buf.readUInt8( 18 ), + floor_max_limit: dcb_buf.readUInt8( 19 ), + floor_max_limit_enabled: !!dcb_buf.readUInt8( 20 ), + device_on: !!dcb_buf.readUInt8( 21 ), + key_lock: !!dcb_buf.readUInt8( 22 ), + run_mode: [ 'heating', 'frost_protection' ][ dcb_buf.readUInt8( 23 ) ], + away_mode: !!dcb_buf.readUInt8( 24 ), + holiday_enabled: !!dcb_buf.readUInt8( 30 ), + holiday_return_date: {}, + temp_hold_minutes: dcb_buf.readUInt16LE( 31 ), + remote_air_temp: dcb_buf.readUInt16LE( 33 ) === 0xFFFF ? null : dcb_buf.readUInt16LE( 33 ) / 10, + floor_temp: dcb_buf.readUInt16LE( 35 ) === 0xFFFF ? null : dcb_buf.readUInt16LE( 35 ) / 10, + built_in_air_temp: dcb_buf.readUInt16LE( 37 ) === 0xFFFF ? null : dcb_buf.readUInt16LE( 37 ) / 10, + error_code: dcb_buf.readUInt8( 39 ), + heating_on: !!dcb_buf.readUInt8( 40 ), + boost_in_min: dcb_buf.readUInt16LE( 41 ), + hot_water_on: !!((model == 'PRTHW') ? dcb_buf.readUInt8( 43 ) : false), + current_time: extract_date( dcb_buf, model ), + } } -var extract_date = function(dcb_buf, model){ - var offset = (model == 'PRTHW') ? 3 : 0 - return new Date(2000 + dcb_buf.readUInt8(41 + offset), dcb_buf.readUInt8(42 + offset) -1 , dcb_buf.readUInt8(43 + offset), dcb_buf.readUInt8(45 + offset), dcb_buf.readUInt8(46 + offset), dcb_buf.readUInt8(47 + offset)) +var extract_date = function ( dcb_buf, model ) { + var offset = (model == 'PRTHW') ? 3 : 0 + return new Date( 2000 + dcb_buf.readUInt8( 41 + offset ), dcb_buf.readUInt8( 42 + offset ) - 1, dcb_buf.readUInt8( 43 + offset ), dcb_buf.readUInt8( 45 + offset ), dcb_buf.readUInt8( 46 + offset ), dcb_buf.readUInt8( 47 + offset ) ) } -var parse_response = function(response){ - var code = response.readUInt8(0); - if(code != 0x94) throw "Invalid return code"; - var frame_len = response.readUInt16LE(1); - if(frame_len != response.length) throw "Incorrect packet length"; - var crc = response.readUInt16LE(frame_len - 2); - var calc_crc = crc16(response.slice(0, frame_len - 2)); - if(crc != calc_crc) throw "Incorrect CRC"; +var parse_response = function ( response ) { + var code = response.readUInt8( 0 ); + if ( code != 0x94 ) throw "Invalid return code"; + var frame_len = response.readUInt16LE( 1 ); + if ( frame_len != response.length ) throw "Incorrect packet length"; + var crc = response.readUInt16LE( frame_len - 2 ); + var calc_crc = crc16( response.slice( 0, frame_len - 2 ) ); + if ( crc != calc_crc ) throw "Incorrect CRC"; - return { - code: code, - frame_len: frame_len, - crc: crc, - start_addr: response.readUInt16LE(3), - num_bytes: response.readUInt16LE(5), - dcb: parse_dcb(response.slice(7, frame_len - 2)) - } + return { + code: code, + frame_len: frame_len, + crc: crc, + start_addr: response.readUInt16LE( 3 ), + num_bytes: response.readUInt16LE( 5 ), + dcb: parse_dcb( response.slice( 7, frame_len - 2 ) ) + } } // Construct an arbitrary thermostat command -Wifi.prototype.command = function(operation, data, callback) { - var 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 +Wifi.prototype.command = function ( operation, data, callback ) { + var 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); - }); + var client = net.connect( { host: this.host, port: this.port }, function () { //'connect' listener + client.write( buf ); + } ); - client.setTimeout(3000); - client.on('data', function(data) { - var obj = parse_response(data); - this.model = obj.dcb.model; - // if callback is set don't emit an event - if (typeof callback === 'undefined') { - this.emit('success', obj); - } else { - callback(obj); - } - client.end(); - }.bind(this)); - client.on('timeout', function(e){ - client.end(); - this.emit('error', (typeof e === 'undefined') ? new Error("Timed out") : e); - }.bind(this)); - client.on('error', function(e){ - client.end(); - this.emit('error', e); - }.bind(this)); + client.setTimeout( 3000 ); + client.on( 'data', function ( data ) { + var obj = parse_response( data ); + this.model = obj.dcb.model; + // if callback is set don't emit an event + if ( typeof callback === 'undefined' ) { + this.emit( 'success', obj ); + } + else { + callback( obj ); + } + client.end(); + }.bind( this ) ); + client.on( 'timeout', function ( e ) { + client.end(); + this.emit( 'error', (typeof e === 'undefined') ? new Error( "Timed out" ) : e ); + }.bind( this ) ); + client.on( 'error', function ( e ) { + client.end(); + this.emit( 'error', e ); + }.bind( this ) ); } -Wifi.prototype.read_device = function(callback){ - this.info(callback); +Wifi.prototype.read_device = function ( callback ) { + this.info( callback ); } -Wifi.prototype.info = function(callback){ - this.command(0x93, new Buffer([0x00, 0x00, 0xFF, 0xFF]), callback); +Wifi.prototype.info = function ( callback ) { + this.command( 0x93, new Buffer( [ 0x00, 0x00, 0xFF, 0xFF ] ), callback ); } -Wifi.prototype.write_device = function(data) { - var self = this; +Wifi.prototype.write_device = function ( data ) { + var self = this; - var do_write = function(items) { - 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 do_write = function ( items ) { + 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 ); - self.command(0xa3, buffer); - } + self.command( 0xa3, buffer ); + } - if (this.model == null) { - this.read_device(function(){ - try { - status_to_dcb(this.model, data, do_write); - } catch (e) { - this.emit('error', e); - } - }); - } else { - try { - status_to_dcb(this.model, data, do_write); - } catch (e) { - this.emit('error', e); + if ( this.model == null ) { + this.read_device( function () { + try { + status_to_dcb( this.model, data, do_write ); + } + catch ( e ) { + this.emit( 'error', e ); + } + } ); + } + else { + try { + status_to_dcb( this.model, data, do_write ); + } + catch ( e ) { + this.emit( 'error', e ); + } } - } } -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') { - if (data % 1 == 0) { - // integer - buf.writeUInt8(data, 3); - } else { - // float, wrong type - throw "Float value not valid, must be integer: " + data; +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' ) { + if ( data % 1 == 0 ) { + // integer + buf.writeUInt8( data, 3 ); + } + else { + // float, wrong type + throw "Float value not valid, must be integer: " + data; + } + } + else { + data.copy( buf, 3 ); } - } - else { - data.copy(buf, 3); - } - return buf; + return buf; } -var timeToByteBuffer = function(hours, minutes) { - var buf = new Buffer(2); - buf[0] = hours; - buf[1] = minutes; - 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 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, callback) { - var items = []; +var status_to_dcb = function ( model, data, callback ) { + var items = []; - var keys = Object.keys(data); - for (var i=0; i{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 "Unsupported item for writing: " + key; } - 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 "Unsupported item for writing: " + key; } - } - callback(items); + callback( items ); } module.exports = Wifi; diff --git a/package.json b/package.json index bad88a3..9400c81 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "author": "Ben Pirt, Carlos Sanchez", "license": "MIT", "readmeFilename": "README.md", - "dependencies" : { + "dependencies": { "ip": "1.0.1" }, "devDependencies": { diff --git a/test/wifi.js b/test/wifi.js index 342ebcb..f55a742 100755 --- a/test/wifi.js +++ b/test/wifi.js @@ -1,150 +1,152 @@ #!/usr/bin/env node -var expect = require("expect.js"), - sinon = require("sinon"), - net = require('net'), - heatmiser = require("../lib/heatmiser"); +var expect = require( "expect.js" ), + sinon = require( "sinon" ), + net = require( 'net' ), + heatmiser = require( "../lib/heatmiser" ); -describe('heatmiser wifi', function(){ +describe( 'heatmiser wifi', function () { - // pin in hex little endian: 0xd204 - var hm = new heatmiser.Wifi('localhost', 1234, 8068, 'PRT-E'); + // pin in hex little endian: 0xd204 + var hm = new heatmiser.Wifi( 'localhost', 1234, 8068, 'PRT-E' ); - // stub sockets - var stub = null; - var socket = null; + // stub sockets + var stub = null; + var socket = null; - beforeEach(function(){ - socket = new net.Socket({}); - sinon.stub(socket, 'setTimeout'); + beforeEach( function () { + socket = new net.Socket( {} ); + sinon.stub( socket, 'setTimeout' ); - socket.on('data', function(data) { - // data.should.eql('foo'); - // test is done or it will timeout and fail - // done(); - }); + socket.on( 'data', function ( data ) { + // data.should.eql('foo'); + // test is done or it will timeout and fail + // done(); + } ); - // Stub the net.connect function - sinon.stub(net, 'connect'); - // Asynchronously call the second argument with a null error and some text when passed certain arguments - net.connect.callsArgWithAsync(1).returns(socket); + // Stub the net.connect function + sinon.stub( net, 'connect' ); + // Asynchronously call the second argument with a null error and some text when passed certain arguments + net.connect.callsArgWithAsync( 1 ).returns( socket ); - hm.on('success', function(data) {}); - hm.on('error', function(error) { throw error; }); - }) + hm.on( 'success', function ( data ) { + } ); + hm.on( 'error', function ( error ) { + throw error; + } ); + } ) - // When done make sure you restore stubs to the original functionality. - afterEach(function(){ - net.connect.restore(); - if (stub != null) stub.restore(); - }) + // When done make sure you restore stubs to the original functionality. + afterEach( function () { + net.connect.restore(); + if ( stub != null ) stub.restore(); + } ) + + describe( '#Heatmiser()', function () { + + it( 'should return host, port and pin', function () { + expect( hm.host ).to.be( 'localhost' ); + expect( hm.pin ).to.be( 1234 ); + expect( hm.port ).to.be( 8068 ); + } ) + + } ); + + describe( '#read_device()', function () { + + it( 'should read the thermostat', function ( done ) { + + stub = sinon.stub( socket, 'write', function ( data, encoding, cb ) { + + // operation: 0x93 + // data length: 0x0b00 + // pin: 0xd204 + // data: 0x0000ffff + // checksum: 0x28b4 + expect( data.toString( 'hex' ) ).to.be( "930b00d2040000ffff28b4" ); + done(); + } ); + + hm.read_device(); + } ) + + } ); + + describe( '#write_device()', function () { + + it( 'should set the thermostat time', function ( done ) { + + stub = sinon.stub( socket, 'write', function ( data, encoding, cb ) { + + // operation: 0xA3 + // data length: 0x1200 + // pin: 0xD204 + // items: 0x01 + // data => position: 0x2B00 size: 0x07 data: 0x0D0C1903132601 (13-12-25 wed 19:38:01) + // checksum: 0x3E1C + expect( data.toString( 'hex' ) ).to.be( "A31200D204012B00070D0C19031326013E1C".toLowerCase() ); + done(); + } ); + + var dcb = { + time: new Date( 2013, 11, 25, 19, 38, 01 ) + } + + hm.write_device( dcb ); + } ) + + it( 'should set the thermostat to frost mode (away)', function ( done ) { + + stub = sinon.stub( socket, 'write', function ( data, encoding, cb ) { + + // operation: 0xA3 + // data length: 0x0C00 + // pin: 0xD204 + // items: 0x01 + // data => position: 0x1700 size: 0x01 data: 0x01 (frost) + // checksum: 0x38DC + expect( data.toString( 'hex' ) ).to.be( "A30C00D204011700010138DC".toLowerCase() ); + + done(); + } ); + + var dcb = { + runmode: 'frost_protection' + } + + hm.write_device( dcb ); + } ) + + it( 'should set the thermostat to temperature hold and limit floor', function ( done ) { + + stub = sinon.stub( socket, 'write', function ( data, encoding, cb ) { + + // operation: 0xA3 + // data length: 0x1500 + // pin: 0xD204 + // items: 0x03 + // data => + // position: 0x1200 size: 0x01 data: 0x14 (20C) + // position: 0x2000 size: 0x02 data: 0x1E00 (30 minutes) + // position: 0x1300 size: 0x01 data: 0x17 (23 C) + // checksum: 0x6CC1 + expect( data.toString( 'hex' ) ).to.be( "A31500D20403120001142000021E00130001176CC1".toLowerCase() ); - describe('#Heatmiser()', function(){ + done(); + } ); - it('should return host, port and pin', function(){ - expect(hm.host).to.be('localhost'); - expect(hm.pin).to.be(1234); - expect(hm.port).to.be(8068); - }) + var dcb = { + heating: { + target: 20, // C + hold: 30 // minutes + }, + floorlimit: { + floormax: 23 + } + } - }); + hm.write_device( dcb ); + } ) - describe('#read_device()', function(){ - - it('should read the thermostat', function(done){ - - stub = sinon.stub(socket, 'write', function (data, encoding, cb) { - - // operation: 0x93 - // data length: 0x0b00 - // pin: 0xd204 - // data: 0x0000ffff - // checksum: 0x28b4 - expect(data.toString('hex')).to.be("930b00d2040000ffff28b4"); - done(); - }); - - hm.read_device(); - }) - - }); - - - describe('#write_device()', function(){ - - it('should set the thermostat time', function(done){ - - stub = sinon.stub(socket, 'write', function (data, encoding, cb) { - - // operation: 0xA3 - // data length: 0x1200 - // pin: 0xD204 - // items: 0x01 - // data => position: 0x2B00 size: 0x07 data: 0x0D0C1903132601 (13-12-25 wed 19:38:01) - // checksum: 0x3E1C - expect(data.toString('hex')).to.be("A31200D204012B00070D0C19031326013E1C".toLowerCase()); - done(); - }); - - var dcb = { - time: new Date(2013,11,25,19,38,01) - } - - hm.write_device(dcb); - }) - - it('should set the thermostat to frost mode (away)', function(done){ - - stub = sinon.stub(socket, 'write', function (data, encoding, cb) { - - // operation: 0xA3 - // data length: 0x0C00 - // pin: 0xD204 - // items: 0x01 - // data => position: 0x1700 size: 0x01 data: 0x01 (frost) - // checksum: 0x38DC - expect(data.toString('hex')).to.be("A30C00D204011700010138DC".toLowerCase()); - - done(); - }); - - var dcb = { - runmode: 'frost_protection' - } - - hm.write_device(dcb); - }) - - it('should set the thermostat to temperature hold and limit floor', function(done){ - - stub = sinon.stub(socket, 'write', function (data, encoding, cb) { - - // operation: 0xA3 - // data length: 0x1500 - // pin: 0xD204 - // items: 0x03 - // data => - // position: 0x1200 size: 0x01 data: 0x14 (20C) - // position: 0x2000 size: 0x02 data: 0x1E00 (30 minutes) - // position: 0x1300 size: 0x01 data: 0x17 (23 C) - // checksum: 0x6CC1 - expect(data.toString('hex')).to.be("A31500D20403120001142000021E00130001176CC1".toLowerCase()); - - done(); - }); - - var dcb = { - heating: { - target: 20, // C - hold: 30 // minutes - }, - floorlimit: { - floormax: 23 - } - } - - hm.write_device(dcb); - }) - - }); -}) + } ); +} )