UNPKG

homebridge-denon-avr

Version:

Homebridge plugin for controlling Denon AVRs via RS232.

675 lines (541 loc) 19.2 kB
// 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); }