homebridge-denon-avr
Version:
Homebridge plugin for controlling Denon AVRs via RS232.
675 lines (541 loc) • 19.2 kB
JavaScript
// Accessory for controlling Denon AVR via HomeKit
var inherits = require('util').inherits;
var SerialPort = require("serialport");
var Service, Characteristic;
// Use a `\r\n` as a line terminator
var parser = new SerialPort.parsers.Readline({
delimiter: '\r'
});
// need to be global to be used in constructor
var maxVolumeMain;
var minVolumeMain;
module.exports = function (homebridge) {
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
homebridge.registerAccessory("homebridge-denon-rs232-serial", "Denon-RS232-Serial", DenonAVR);
function DenonAVR(log, config) {
// configuration
this.name = config['name'];
this.path = config['path'];
this.mainZoneName = config['mainZoneName'] || "Main Zone";
this.subZoneName = config['subZoneName'] || "Zone 2";
maxVolumeMain = config['maxVolumeMain'];
minVolumeMain = config['minVolumeMain'];
maxVolumeSub = config['maxVolumeSub'] || config['maxVolumeMain'];
minVolumeSub = config['minVolumeSub'] || config['minVolumeMain'];
this.timeout = config.timeout || 1000;
this.queue = [];
this.callbackQueue = [];
this.ready = true;
this.log = log;
this.volumeMain = minVolumeMain;
this.serialPort = new SerialPort(this.path, {
baudRate: 9600,
autoOpen: false
}); // this is the openImmediately flag [default is true]
this.serialPort.pipe(parser);
parser.on('data', function (data) {
this.log("Received data: " + data);
this.serialPort.close(function (error) {
this.log("Closing connection");
if (error) this.log("Error when closing connection: " + error)
var callback;
if (this.callbackQueue.length) callback = this.callbackQueue.shift()
if (callback) callback(data, 0);
}.bind(this)); // close after response
}.bind(this));
}
// Custom Characteristics and service...
DenonAVR.AudioVolume = function () {
Characteristic.call(this, 'Volume', '00001001-0000-1000-8000-135D67EC4377');
this.log("Maximum Volume", maxVolumeMain);
this.setProps({
format: Characteristic.Formats.FLOAT,
maxValue: maxVolumeMain,
minValue: minVolumeMain,
minStep: 1.0,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
this.value = this.getDefaultValue();
};
inherits(DenonAVR.AudioVolume, Characteristic);
DenonAVR.Muting = function () {
Characteristic.call(this, 'Mute', '00001002-0000-1000-8000-135D67EC4377');
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
this.value = this.getDefaultValue();
};
inherits(DenonAVR.Muting, Characteristic);
DenonAVR.AudioDeviceService = function (displayName, subtype) {
Service.call(this, displayName, '00000001-0000-1000-8000-135D67EC4377', subtype);
this.addCharacteristic(DenonAVR.AudioVolume);
this.addCharacteristic(DenonAVR.Muting);
};
inherits(DenonAVR.AudioDeviceService, Service);
DenonAVR.prototype = {
send: function (cmd, callback) {
this.sendCommand(cmd, callback);
//if (callback) callback();
},
exec: function () {
// Check if the queue has a reasonable size
if (this.queue.length > 100) {
this.queue.clear();
this.callbackQueue.clear();
}
this.queue.push(arguments);
this.process();
},
sendCommand: function (command, callback) {
this.log("serialPort.open");
if (this.serialPort.isOpen) {
this.log("serialPort is already open...");
if (callback) callback(0, 1);
} else {
this.serialPort.open(function (error) {
if (error) {
this.log("Error when opening serialport: " + error);
if (callback) callback(0, error);
} else {
if (callback) this.callbackQueue.push(callback);
this.serialPort.write(command, function (err) {
if (err) this.log("Write error = " + err);
//this.serialPort.drain();
}.bind(this));
}
// if(callback) callback(0,0);
}.bind(this));
}
},
process: function () {
if (this.queue.length === 0) return;
if (!this.ready) return;
var self = this;
this.ready = false;
this.send.apply(this, this.queue.shift());
setTimeout(function () {
self.ready = true;
self.process();
}, this.timeout);
},
getPowerState: function (callback) {
var cmd = "PW?\r";
this.log("getPowerState");
this.exec(cmd, function (response, error) {
this.log("Power state is: " + response);
if (response && response.indexOf("PWON") > -1) {
if (callback) callback(null, true);
} else {
if (callback) callback(null, false);
}
}.bind(this))
},
setPowerState: function (powerOn, callback) {
var cmd;
if (powerOn) {
cmd = "PWON\r";
this.log("Set", this.name, "to on");
} else {
cmd = "PWSTANDBY\r";
this.log("Set", this.name, "to off");
}
this.exec(cmd, function (response, error) {
if (error) {
this.log('Serial power function failed: %s');
if (callback) callback(error);
} else {
this.log('Serial power function succeeded!');
if (callback) callback();
}
}.bind(this));
},
getMuteState: function (callback) {
var cmd = "MU?\r";
this.exec(cmd, function (response, error) {
this.log("Mute state is:", response);
if (response && response.indexOf("MU") > -1) {
callback(null, true);
} else {
callback(null, false);
}
}.bind(this))
},
setMuteState: function (muteOn, callback) {
var cmd;
if (muteOn) {
cmd = "MUON\r";
this.log(this.name, "muted");
} else {
cmd = "MUOFF\r";
this.log(this.name, "unmuted");
}
this.exec(cmd, function (response, error) {
if (error) {
this.log('Serial mute function failed: %s');
callback(error);
} else {
this.log('Serial mute function succeeded!');
callback();
}
}.bind(this));
},
dbToPercentage: function (db, max, min) {
this.log("dbToPercentage");
var minMaxDiff = max - min;
this.log("db = " + db);
var percentage = 100.0 * (db - min) / minMaxDiff;
this.log("percentage = " + percentage);
return percentage;
},
percentageToDb: function (percentage, max, min) {
this.log("percentageToDb");
var minMaxDiff = max - min;
this.log("percentage = " + percentage);
var db = 0.01 * percentage * minMaxDiff + min;
if (db > max) db = max;
if (db < min) db = min;
this.log("db = " + db);
return db;
},
getVolume: function (callback) {
var cmd = "MV?\r";
this.exec(cmd, function (response, error) {
//VOL:xxxy(xxx)
if (response && response.indexOf("MV") > -1) {
var vol = 0;
/*
if (response.indexOf("+") > -1) {
//console.log("+");
vol = response.substring(6, 8);
} else {
//console.log("-");
vol = response.substring(5, 8);
}*/
vol = response.substring(2, 4);
this Main = this.dbToPercentage(Number(vol), maxVolumeMain, minVolumeMain);
//console.log("this.volumeMain=" + this.volumeMain);
callback(null, Number(this.volumeMain));
} else callback(null, 0);
}.bind(this))
},
setVolume: function (value, callback) {
var db = this.percentageToDb(value, maxVolumeMain, minVolumeMain);
if (this.volumeMain != value) {
this.volumeMain = value;
var cmd = "MV";
//if (db > 0) cmd = cmd + "+";
cmd = cmd + parseInt(db * 10.0);
cmd = cmd + "\r";
this.exec(cmd, function (response, error) {
if (error) {
this.log('Serial volume function failed: %s');
callback(error);
} else {
this.log("Set volume to", db, "db");
callback();
}
}.bind(this));
} else {
this.log("Volume has not changed");
callback();
}
},
getVolumeUpState: function (callback) {
callback(null, 0);
},
getVolumeDownState: function (callback) {
callback(null, 0);
},
getVolumeUpFastState: function (callback) {
callback(null, 0);
},
getVolumeDownFastState: function (callback) {
callback(null, 0);
},
setVolumeUpState: function (value, callback) {
var cmd = "MVUP\r";
var signedValue = value;
this.setVolumeState(cmd, signedValue, callback);
var targetChar = this.mainZoneService.getCharacteristic(VolumeUpCharacteristic);
if (value > 0) setTimeout(function () {
targetChar.setValue(false);
}, 300);
},
setVolumeDownState: function (value, callback) {
var cmd = "MVDOWN\r";
var signedValue = -1 * value;
this.setVolumeState(cmd, signedValue, callback);
var targetChar = this.mainZoneService.getCharacteristic(VolumeDownCharacteristic);
if (value > 0) setTimeout(function () {
targetChar.setValue(false);
}, 300);
},
setVolumeUpFastState: function (value, callback) {
var cmd = "MVUP\r";
var signedValue = value;
this.setVolumeState(cmd, signedValue, callback);
var targetChar = this.mainZoneService.getCharacteristic(VolumeUpFastCharacteristic);
if (value > 0) setTimeout(function () {
targetChar.setValue(false);
}, 300);
},
setVolumeDownFastState: function (value, callback) {
var cmd = "MVDOWN\r";
var signedValue = -1 * value;
this.setVolumeState(cmd, signedValue, callback);
var targetChar = this.mainZoneService.getCharacteristic(VolumeDownFastCharacteristic);
if (value > 0) setTimeout(function () {
targetChar.setValue(false);
}, 300);
},
setVolumeState: function (cmd, value, callback) {
if (value == 0) {
this.log("Resetting volume up/down button");
callback();
} else if (value > 0 && this.volumeMain >= 100) {
this.log("Maximum volume reached");
callback(); // limit the volume
} else if (value < 0 && this.volumeMain <= 0) {
this.log("Minumum volume reached");
callback(); // limit the volume
} else {
this.log('Executing: ' + cmd);
this.exec(cmd, function (response, error) {
if (error) {
this.log('Serial increase volume function failed: ' + error);
callback(error);
} else {
this.log("Changing volume");
var targetCharVol = this.mainZoneService.getCharacteristic(Characteristic.Volume);
targetCharVol.getValue(null);
callback();
}
}.bind(this));
}
},
getSourcePort: function (callback) {
var cmd = "SI\r";
this.exec(cmd, function (response, error) {
//SRC:xx
if (response && response.indexOf("SI") > -1) {
var src = response.substring(2, 100);
var srcNr = 0;
if (src == 'PHONO') srcNr = 10;
else if (src == 'CD') srcNr = 11;
else if (src == 'TUNER') srcNr = 12;
else if (src == 'DVD') srcNr = 13;
else if (src == 'VDP') srcNr = 14;
else if (src == 'TV') srcNr = 15;
else if (src == 'DBS/SAT') srcNr = 16;
else if (src == 'DBS') srcNr = 17;
else if (src == 'VCR-1') srcNr = 18;
else if (src == 'VCR-2') srcNr = 19;
else if (src == 'VCR-3') srcNr = 20;
else if (src == 'V.AUX') srcNr = 21;
else if (src == 'CDR/TAPE') srcNr = 22;
else if (src == 'CDR/TAPE1') srcNr = 23;
else if (src == 'MD/TAPE2') srcNr = 24;
else srcNr = Number(src);
//console.log("src =" + src + " srcNr = " + srcNr);
callback(null, srcNr);
} else callback(null, 0);
}.bind(this))
},
setSourcePort: function (port, callback) {
var cmd = "SI";
if (port < 10) cmd = cmd + port + "\r";
else if (port == 10) cmd = cmd + 'PHONO' + "\r";
else if (port == 11) cmd = cmd + 'CD' + "\r";
else if (port == 12) cmd = cmd + 'TUNER' + "\r";
else if (port == 13) cmd = cmd + 'DVD' + "\r";
else if (port == 14) cmd = cmd + 'VDP' + "\r";
else if (port == 15) cmd = cmd + 'TV' + "\r";
else if (port == 16) cmd = cmd + 'DBS/SAT' + "\r";
else if (port == 17) cmd = cmd + 'DBS' + "\r";
else if (port == 18) cmd = cmd + 'VCR-1' + "\r";
else if (port == 19) cmd = cmd + 'VCR-2' + "\r";
else if (port == 20) cmd = cmd + 'VCR-3' + "\r";
else if (port == 21) cmd = cmd + 'V.AUX' + "\r";
else if (port == 22) cmd = cmd + 'CDR/TAPE' + "\r";
else if (port == 23) cmd = cmd + 'CDR/TAPE1' + "\r";
else if (port == 24) cmd = cmd + 'MD/TAPE2' + "\r";
else cmd = cmd + 0 + "\r";
this.exec(cmd, function (response, error) {
if (error) {
this.log('Set Source function failed: ' + error);
callback(error);
} else {
this.log('Set Source function succeeded!');
callback();
}
}.bind(this));
},
identify: function (callback) {
this.log("Identify requested!");
this.setPowerState(true); // turn on
this.toggleTestTone();
this.toggleTestTone(callback);
if (callback) callback();
},
toggleTestTone: function (callback) {
var cmd = "@TTO:0\r";
this.exec(cmd); // send without callback
cmd = "@VOL:?\r"; // get confirmation with callback
this.exec(cmd, function (response, error) {
if (error) {
this.log('Serial volume function failed: %s');
if (callback) callback(error);
} else {
this.log("Toggle test tone");
if (callback) callback();
}
}.bind(this));
},
getServices: function () {
var that = this;
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Name, this.name)
.setCharacteristic(Characteristic.Manufacturer, "Denon")
.setCharacteristic(Characteristic.Model, "AVR 3803")
.setCharacteristic(Characteristic.SerialNumber, "1234567890");
var switchService = new Service.Switch(this.mainZoneName, "power_on");
switchService
.getCharacteristic(Characteristic.On)
.on('get', this.getPowerState.bind(this))
.on('set', this.setPowerState.bind(this));
makeHSourceCharacteristic(homebridge, "main");
switchService
.addCharacteristic(SourceCharacteristic)
.on('get', this.getSourcePort.bind(this))
.on('set', this.setSourcePort.bind(this));
var mainZoneService = new Service.Speaker(this.mainZoneName);
mainZoneService
.getCharacteristic(Characteristic.Mute)
.on('get', this.getMuteState.bind(this))
.on('set', this.setMuteState.bind(this));
mainZoneService
.getCharacteristic(Characteristic.Volume)
.on('get', this.getVolume.bind(this))
.on('set', this.setVolume.bind(this));
makeVolumeControlCharacteristic(homebridge, "main");
mainZoneService
.addCharacteristic(VolumeUpCharacteristic)
.on('get', this.getVolumeUpState.bind(this))
.on('set', this.setVolumeUpState.bind(this));
mainZoneService
.addCharacteristic(VolumeDownCharacteristic)
.on('get', this.getVolumeDownState.bind(this))
.on('set', this.setVolumeDownState.bind(this));
mainZoneService
.addCharacteristic(VolumeUpFastCharacteristic)
.on('get', this.getVolumeUpFastState.bind(this))
.on('set', this.setVolumeUpFastState.bind(this));
mainZoneService
.addCharacteristic(VolumeDownFastCharacteristic)
.on('get', this.getVolumeDownFastState.bind(this))
.on('set', this.setVolumeDownFastState.bind(this));
this.mainZoneService = mainZoneService;
makeHSourceCharacteristic(homebridge, "sub");
switchService
.addCharacteristic(SourceCharacteristic)
.on('get', this.getSourcePort.bind(this))
.on('set', this.setSourcePort.bind(this));
var subZoneService = new Service.Speaker(this.subZoneName);
subZoneService
.getCharacteristic(Characteristic.Mute)
.on('get', this.getMuteState.bind(this))
.on('set', this.setMuteState.bind(this));
subZoneService
.getCharacteristic(Characteristic.Volume)
.on('get', this.getVolume.bind(this))
.on('set', this.setVolume.bind(this));
makeVolumeControlCharacteristic(homebridge, "sub");
subZoneService
.addCharacteristic(VolumeUpCharacteristic)
.on('get', this.getVolumeUpState.bind(this))
.on('set', this.setVolumeUpState.bind(this));
subZoneService
.addCharacteristic(VolumeDownCharacteristic)
.on('get', this.getVolumeDownState.bind(this))
.on('set', this.setVolumeDownState.bind(this));
subZoneService
.addCharacteristic(VolumeUpFastCharacteristic)
.on('get', this.getVolumeUpFastState.bind(this))
.on('set', this.setVolumeUpFastState.bind(this));
subZoneService
.addCharacteristic(VolumeDownFastCharacteristic)
.on('get', this.getVolumeDownFastState.bind(this))
.on('set', this.setVolumeDownFastState.bind(this));
this.subZoneService = subZoneService;
return null; //[informationService, switchService, mainZoneService, mainZoneService];
}
}
};
function makeHSourceCharacteristic(homebridge, zonename) {
UUID = homebridge.hap.uuid;
SourceCharacteristic = function () {
var serviceUUID = UUID.generate('DenonTypes:' + zonename + 'VolumeControl:Source');
Characteristic.call(this, 'Source', serviceUUID);
this.setProps({
format: Characteristic.Formats.INT,
unit: Characteristic.Units.NONE,
maxValue: 23,
minValue: 0,
minStep: 1,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
//this.eventEnabled = true;
this.value = this.getDefaultValue();
};
inherits(SourceCharacteristic, Characteristic);
}
function makeVolumeControlCharacteristic(homebridge, zonename) {
UUID = homebridge.hap.uuid;
VolumeUpCharacteristic = function () {
var serviceUUID = UUID.generate('DenonTypes:' + zonename + 'MainVolumeControl:VolumeUp');
Characteristic.call(this, 'Volume Up', serviceUUID);
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
//this.eventEnabled = true;
this.value = this.getDefaultValue();
};
inherits(VolumeUpCharacteristic, Characteristic);
VolumeDownCharacteristic = function () {
var serviceUUID = UUID.generate('DenonTypes:' + zonename + 'MainVolumeControl:VolumeDown');
Characteristic.call(this, 'Volume Down', serviceUUID);
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
//this.eventEnabled = true;
this.value = this.getDefaultValue();
};
inherits(VolumeDownCharacteristic, Characteristic);
VolumeUpFastCharacteristic = function () {
var serviceUUID = UUID.generate('DenonTypes:' + zonename + 'MainVolumeControl:VolumeUpFast');
Characteristic.call(this, 'Volume Up Fast', serviceUUID);
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
//this.eventEnabled = true;
this.value = this.getDefaultValue();
};
inherits(VolumeUpFastCharacteristic, Characteristic);
VolumeDownFastCharacteristic = function () {
var serviceUUID = UUID.generate('DenonTypes:' + zonename + 'MainVolumeControl:VolumeDownFast');
Characteristic.call(this, 'Volume Down Fast', serviceUUID);
this.setProps({
format: Characteristic.Formats.BOOL,
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
});
//this.eventEnabled = true;
this.value = this.getDefaultValue();
};
inherits(VolumeDownFastCharacteristic, Characteristic);
}