home-controller
Version:
A Node Home Automation Controller for Insteon Devices
555 lines (424 loc) • 11.9 kB
JavaScript
var utils = require('./utils');
var toByte = utils.toByte;
var convertTemp = utils.convertTemp;
var util = require('util');
var Q = require('q');
var events = require('events');
var debug = require('debug')('home-controller:insteon:thermostat');
function Thermostat(id, insteon) {
this.id = id;
this.insteon = insteon;
}
util.inherits(Thermostat, events.EventEmitter);
Thermostat.prototype.tempUp = function (change, next) {
if(typeof change === 'function') {
next = change;
change = 1;
}
if(change === null || change === undefined) {
change = 1;
}
change = toByte(change * 2);
var cmd = {
cmd1: '68',
cmd2: change,
extended: true,
isStandardResponse: true
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.tempDown = function (change, next) {
if(typeof change === 'function') {
next = change;
change = 1;
}
if(change === null || change === undefined) {
change = 1;
}
change = toByte(change * 2);
var cmd = {
cmd1: '69',
cmd2: change,
extended: true,
isStandardResponse: true
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.info = function (type, divisor, zone, next) {
if(typeof zone === 'function') {
next = zone;
zone = 0;
}
zone = zone || 0;
var cmd2 = toByte(type << 5 | zone);
var deferred = Q.defer();
deferred.resolve(
this.insteon.directCommand(this.id, '6A', cmd2)
.then(function (status) {
return parseInt(status.response.standard.command2, 16)/divisor;
})
);
return deferred.promise.nodeify(next);
};
Thermostat.prototype.temp = function (zone, next) {
return this.info(0, 2, zone, next);
};
Thermostat.prototype.setpoints = function (zone, next) {
if(typeof zone === 'function') {
next = zone;
zone = 0;
}
if(!zone) {
zone = 0;
}
var cmd2 = toByte(1 << 5 | zone);
var deferred = Q.defer();
deferred.resolve(
this.insteon.directCommand(this.id, {cmd1: '6A', cmd2: cmd2, responseCount: 2})
.then(function (status) {
return status.response.standard.map(function (resp) {
return parseInt(resp.command2, 16) / 2;
});
})
);
return deferred.promise.nodeify(next);
};
Thermostat.prototype.humidity = function (zone, next) {
return this.info(3, 1, zone, next);
};
var MODES = [
'off',
'heat',
'cool',
'auto',
'fan',
'program',
'program heat',
'program cool',
'fan auto'
];
var MODE_CMDS = {
off: '09',
heat: '04',
cool: '05',
auto: '06',
fan: '07',
'fan auto': '08',
'program heat': '0A',
'program cool': '0B',
'program auto': '0C'
};
Thermostat.prototype.mode = function (mode, next) {
if(typeof mode === 'function') {
next = mode;
mode = null;
}
if(mode) {
var modeCmd = MODE_CMDS[mode.toLowerCase()];
if(!modeCmd) {
return Q.fcall(function() {
throw new Error('Invalid mode. Must be one of the following: ' + MODES.join(', '));
}).nodeify(next);
}
var cmd = {
cmd1: '6B',
cmd2: modeCmd,
extended: true,
exitOnAck: true
};
return this.insteon.directCommand(this.id, cmd)
.then(function(status) {
if(!!status && status.success) {
return mode;
} else {
return null;
}
})
.nodeify(next);
}
var deferred = Q.defer();
deferred.resolve(
this.insteon.directCommand(this.id, '6B', '02')
.then( function (status) {
var mode = parseInt(status.response.standard.command2, 16);
return MODES[mode];
})
);
return deferred.promise.nodeify(next);
};
Thermostat.prototype.coolTemp = function (temp, next) {
temp = toByte(temp * 2);
var cmd = {
cmd1: '6C',
cmd2: temp,
extended: true,
isStandardResponse: true
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.heatTemp = function (temp, next) {
temp = toByte(temp * 2);
var cmd = {
cmd1: '6D',
cmd2: temp,
extended: true,
isStandardResponse: true
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.highHumidity = function (humidity, next) {
var cmd = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['01','0B', toByte(humidity)]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.lowHumidity = function (humidity, next) {
var cmd = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['01','0C', toByte(humidity)]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.backlight = function (delay, next) {
var cmd = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['01','05', toByte(delay)]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.cycleDelay = function (delay, next) {
var cmd = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['01','06', toByte(delay)]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.energyChange = function (change, next) {
var cmd = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['01','07', toByte(change)]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.date = function (date, next) {
if(typeof date === 'function') {
next = date;
date = new Date();
}
if(!util.isDate(date)) {
date = new Date(date);
}
var cmd = {
cmd1: '2E',
cmd2: '02',
extended: true,
exitOnAck: true,
crc: true,
userData: [
'02',
toByte(date.getDay()),
toByte(date.getHours()),
toByte(date.getMinutes()),
toByte(date.getSeconds())
]
};
return this.insteon.directCommand(this.id, cmd, next);
};
Thermostat.prototype.details = function (next) {
var cmdDataSet00 = {
cmd1: '2E',
cmd2: '00',
extended: true,
crc: true
};
var cmdDataSet01 = {
cmd1: '2E',
cmd2: '00',
extended: true,
crc: true,
userData: ['00', '00', '01']
};
var insteon = this.insteon;
var id = this.id;
var details = {};
var data;
var deferred = Q.defer();
deferred.resolve(
this.status()
.then(function (status) {
if(!!status) {
details = status;
} else {
debug('No response for status command.');
return;
}
}).
then(function() {
return insteon.directCommand(id, cmdDataSet00);
})
.then(function (status) {
if(!status || !status.response || !status.response.extended) {
debug('No response for status command set #1');
return;
}
data = status.response.extended.userData;
details.backlight = parseInt(data[9], 16);
details.delay = parseInt(data[10], 16);
details.energyOffset = parseInt(data[11], 16);
})
.then( function () {
return insteon.directCommand(id, cmdDataSet01);
})
.then(function (status) {
if(!status || !status.response || !status.response.extended) {
debug('No response for status command set #2');
return details;
}
data = status.response.extended.userData;
details.setpoints = details.setpoints || {};
details.setpoints.highHumidity = parseInt(data[3], 16);
details.setpoints.lowHumidity = parseInt(data[4], 16);
return details;
})
);
return deferred.promise.nodeify(next);
};
Thermostat.prototype.status = function (next) {
var cmd02 = {
cmd1: '2E',
cmd2: '02',
extended: true,
crc: true
};
var insteon = this.insteon;
var id = this.id;
var details = {};
var data;
var deferred = Q.defer();
deferred.resolve(
insteon.directCommand(id, cmd02)
.then(function (status) {
if(!status || !status.response || !status.response.extended) {
debug('No response for status command.');
return null;
}
if(!status.response.extended.userData || !status.response.extended.crc) {
return null;
}
data = status.response.extended.userData;
details.date = {
day: parseInt(data[1], 16),
hour: parseInt(data[2], 16),
minute: parseInt(data[3], 16),
seconds: parseInt(data[4], 16)
};
var rawMode = parseInt(data[5], 16);
var modes = ['off', 'auto', 'heat', 'cool', 'program'];
details.mode = modes[rawMode >> 4];
details.fan = !!(rawMode & 1);
details.setpoints = {
cool: convertTemp('F', insteon.defaultTempUnits, parseInt(data[6], 16)),
heat: convertTemp('F', insteon.defaultTempUnits, parseInt(data[11], 16))
};
details.humidity = parseInt(data[7], 16);
var tempC = parseInt(data[8] + data[9], 16) / 10;
details.temperature = convertTemp('C', insteon.defaultTempUnits, tempC);
var rawState = parseInt(data[10], 16);
details.cooling = !!(rawState & 1);
details.heating = !!((rawState >> 1) & 1);
details.energySaving = !!((rawState >> 2) & 1);
details.unit = ((rawState >> 3) & 1) ? 'C' : 'F';
details.hold = !!((rawState >> 4) & 1);
return details;
})
);
return deferred.promise.nodeify(next);
};
Thermostat.prototype.monitor = function (enable, next) {
if(typeof enable === 'function')
{
next = enable;
enable = true;
}
if(enable === null || enable === undefined) {
enable = true;
}
var id = this.id;
var insteon = this.insteon;
if(enable) {
return insteon.scene(id, 'gw', {group: 0xEF})
.then(function() {
var cmdDataSet08 = {
cmd1: '2E',
cmd2: '00',
extended: true,
exitOnAck: true,
userData: ['08']
};
return insteon.directCommand(id, cmdDataSet08, next);
});
}
return insteon.scene(id, null, {group: 0xEF, remove: true}, next);
};
Thermostat.prototype.handleDirect = function (cmd1, cmd2) {
this.emit('command', cmd1, cmd2);
switch(cmd1) {
case '6e':
this.emit('status', { 'temperature': convertTemp('F', this.insteon.defaultTempUnits, 0.5 * cmd2) });
break;
case '6f':
this.emit('status', { 'humidity': cmd2 });
break;
case '70':
var fan = !!(cmd2 & 0x10);
var modes = ['off', 'heat', 'cool', 'auto', 'program'];
var mode = modes[cmd2 & 0x0F];
this.emit('status', { 'mode': mode, 'fan': fan });
break;
case '71':
this.emit('status', { 'coolSetpoint': cmd2 });
break;
case '72':
this.emit('status', { 'heatSetpoint': cmd2 });
break;
default:
debug('No event for command - %s', cmd1);
}
};
Thermostat.prototype.handleAllLinkBroadcast = function (group, cmd1) {
this.emit('command', group, cmd1);
switch(cmd1) {
case '11':
this.emit(['cooling', 'heating', 'highHumidity', 'lowHumidity'][group-1]);
break;
case '13':
if(group === 3 || group === 4) {
this.emit('normalHumidity');
} else {
this.emit('off');
}
break;
default:
debug('No event for command - %s', cmd1);
}
};
Thermostat.prototype.cancelPending = function() {
this.insteon.cancelPending(this.id);
};
module.exports = Thermostat;