marantz-denon-telnet
Version:
npm package to control marantz and Denon AVR over good ol' telnet.
637 lines (540 loc) • 22.2 kB
JavaScript
/**
@fileoverview Control marantz and Denon AVR's by telnet.
@author Mike Kronenberg <mike.kronenberg@kronenberg.org> (http://www.kronenberg.org)
@license MIT
*/
/**
Function called when a command is run and data is returned.
@callback defaultCallback
@param {Error} error NULL or Error object, if command failed
@param {null|Object|Array|boolean} data Array with returned data or NULL if command failed
*/
var telnet = require('telnet-client');
/**
Returns an instance of MarantzDenonTelnet that can handle telnet commands to the given IP.
@constructor
@param {string} ip Address of the AVR.
*/
var MarantzDenonTelnet = function(ip) {
this.connectionparams = {
host: ip,
port: 23,
timeout: 1000, // The RESPONSE should be sent within 200ms of receiving the request COMMAND. (plus network delay)
sendTimeout: 1200,
negotiationMandatory: false,
ors: '\r', // Set outbound record separator
irs: '\r'
};
this.cmdCue = [];
this.connection = null;
};
/**
Search for the required information in returned data array, as they may contain additional and random data from EVENTs.
@param {Array} data Array of possible responses
@param {RegExp} regexp to test against
@return {!string} found string or false
*/
MarantzDenonTelnet.prototype.parseSimpleResponse = function(data, regexp) {
var i;
var ret = false;
var r = false;
for (i = 0; i < data.length; i++) {
if (r = regexp.exec(data[i])) {
return r[1];
}
}
return ret;
};
/**
Works thru Telnet cue.
*/
MarantzDenonTelnet.prototype.sendNextTelnetCueItem = function() {
var mdt = this;
if (this.cmdCue.length) {
if (!this.connection) {
this.connection = new telnet();
this.connection.on('connect', function() {
mdt.sendNextTelnetCueItem();
});
this.connection.on('error', function() {
// There must be an item on the cmdCue, else we wouldn't be here...
// and node.js barfs if we don't catch this error. So lets catch it
// and use the first item in the cmdCue's error handler.
// Only quetion is, should we remove it from the queue or not?
mdt.cmdCue[0].callback('Failed to connect', null);
mdt.connection.end();
mdt.connection = null;
});
this.connection.connect(this.connectionparams);
} else {
var item = this.cmdCue.shift();
var isRequestCommand = (item.cmd.substr(-1) === '?');
var send_options = {timeout: (isRequestCommand ? this.connectionparams.timeout : 10)};
if (typeof item.waitfor !== 'undefined') {
send_options.waitfor = item.waitfor;
}
this.connection.send(item.cmd, send_options, function(error, data) {
//if (isRequestCommand) console.log(`\n\n\n${item.cmd}:${JSON.stringify(data)}`);
if (typeof data === 'string') {
data = data.trim().split('\r');
for (var i = 0; i < data.length; i++) { // sanitize data
data[i] = data[i].trim();
}
} else if (error && !isRequestCommand && error.message === 'response not received') { // if it is no 'request COMMAND' the AVR will not respond
data = [];
error = null;
}
item.callback(error, data);
setTimeout(function() { // Send the COMMAND in 50ms or more intervals.
mdt.sendNextTelnetCueItem();
}, 50);
});
}
} else {
this.connection.end();
this.connection = null;
}
};
/**
Low level method to add a command to the Telnet cue.
@param {string} cmd Telnet command
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?RegExp} waitfor Wait for this regexp to fulfill instead of a timeout.
*/
MarantzDenonTelnet.prototype.telnet = function(cmd, callback, waitfor) {
this.cmdCue.push({'cmd': cmd, 'callback': callback, 'waitfor': waitfor});
if (!this.connection) {
this.sendNextTelnetCueItem();
}
};
/**
Send raw Telnet codes to the AVR.
@see marantz Telnet Reference {@link http://www.us.marantz.com/DocumentMaster/US/Marantz_FY16_AV_SR_NR_PROTOCOL_V01(2).xls}
@param {string} cmd Telnet command
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.cmd('PW?', function(error, data) {console.log('Power is: ' + data);});
// Power is: [PWON,Z2ON,Z3ON|PWSTANDBY]
*/
MarantzDenonTelnet.prototype.cmd = function(cmd, callback) {
this.telnet(cmd, function(error, data) {
if (!error) {
callback(null, data);
} else {
callback(error);
}
});
};
/**
Get the currently selected input of a zone.
Telnet Command examples: SI?, Z2SOURCE
@param {defaultCallback} callback Function to be called when the command is run and data is returned. Will return one or more of: 'CD', 'SPOTIFY', 'CBL/SAT', 'DVD', 'BD', 'GAME', 'GAME2', 'AUX1',
'MPLAY', 'USB/IPOD', 'TUNER', 'NETWORK', 'TV', 'IRADIO', 'SAT/CBL', 'DOCK',
'IPOD', 'NET/USB', 'RHAPSODY', 'PANDORA', 'LASTFM', 'IRP', 'FAVORITES', 'SERVER'
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getInput(function(error, data) {console.log('INPUT of MAIN ZONE is: ' + JSON.stringify(data));}, 'ZM');
// INPUT of MAIN ZONE is: "MPLAY"
*/
MarantzDenonTelnet.prototype.getInput = function(callback, zone) {
var mdt = this;
var commandPrefix = (!zone || (zone == 'ZM')) ? 'SI' : zone;
var regexp = RegExp('(?:^|[\r])' + commandPrefix + '([^O0-9]+[^NF]+)');
this.telnet(commandPrefix + '?', function(error, data) {
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, ret);
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
});
};
/**
Select the input of a zone.
Telnet Command examples: SIMPLAY, Z2MPLAY, Z3CD
@param {string} input Supported values: 'CD', 'SPOTIFY', 'CBL/SAT', 'DVD', 'BD', 'GAME', 'GAME2', 'AUX1',
'MPLAY', 'USB/IPOD', 'TUNER', 'NETWORK', 'TV', 'IRADIO', 'SAT/CBL', 'DOCK',
'IPOD', 'NET/USB', 'RHAPSODY', 'PANDORA', 'LASTFM', 'IRP', 'FAVORITES', 'SERVER'
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setInput('MPLAY', function(error, data) {console.log('Set INPUT of MAIN ZONE to MPLAY.');});
// Set INPUT of MAIN ZONE to MPLAY.
*/
MarantzDenonTelnet.prototype.setInput = function(input, callback, zone) {
var commandPrefix = (!zone || (zone == 'ZM')) ? 'SI' : zone;
this.telnet(commandPrefix + input, function(error, data) {
if (!error) {
callback(null, data);
} else {
callback(error);
}
});
};
/**
Get the current mute state of a zone.
Defaults MAIN ZONE, if no zone set.
Telnet Command examples: SIMPLAY, Z2MPLAY, Z3CD
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getMuteState(function(error, data) {console.log('MUTE state of ZONE2 is: ' + (data ? 'ON' : 'OFF'));}, 'Z2');
// MUTE state of ZONE2 is: [ON|OFF]
*/
MarantzDenonTelnet.prototype.getMuteState = function(callback, zone) {
var mdt = this;
var commandPrefix = (!zone || (zone == 'ZM')) ? '' : zone;
var regexp = RegExp('(?:^|[\r])' + commandPrefix + 'MU(ON|OFF)');
this.telnet(commandPrefix + 'MU?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, (ret == 'ON'));
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
}, regexp);
};
/**
Set the mute state of a zone.
Defaults MAIN ZONE, if no zone set.
Telnet Command examples: MUON, MUOFF, Z2MUON, Z3MUOFF
@param {boolean} muteState TRUE for muted
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setMuteState(true, function(error, data) {console.log('Set MUTE state of ZONE2 to ON.');}, 'Z2');
// Set MUTE state of ZONE2 to ON.
*/
MarantzDenonTelnet.prototype.setMuteState = function(muteState, callback, zone) {
var commandPrefix = (!zone || (zone == 'ZM')) ? '' : zone;
this.telnet(commandPrefix + 'MU' + (muteState ? 'ON' : 'OFF'), function(error, data) {
if (!error) {
callback(null, muteState);
} else {
callback(error);
}
});
};
/**
Get the current power state of the AVR.
Telnet Command examples: PW?
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getPowerState(function(error, data) {console.log('POWER state of AVR is: ' + (data ? 'ON' : 'OFF'))});
// POWER state of AVR is: [ON|OFF]
*/
MarantzDenonTelnet.prototype.getPowerState = function(callback) {
var mdt = this;
var regexp = RegExp(/PW(ON|OFF)/);
this.telnet('PW?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, (ret == 'ON'));
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback("Can't connect to device: " + error, false);
}
}, regexp);
};
/**
Sets the power state of the AVR.
Telnet Command examples: PWON, PWSTANDBY (threr is no PWOFF!)
@param {boolean} powerState - TRUE to power the AVR on
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setPowerState(false, function(error, data) {console.log('Sent power off command to AVR.');});
// Set POWER state of AVR to ON.
*/
MarantzDenonTelnet.prototype.setPowerState = function(powerState, callback) {
this.telnet('PW' + (powerState ? 'ON' : 'STANDBY'), function(error, data) {
if (!error) {
callback(null, powerState);
} else {
callback(error);
}
});
};
/**
Get the currently selected SMART SELECT of a zone.
Telnet Command examples: MSSMART ?, Z2SMART ?
@param {defaultCallback} callback Function to be called when the command is run and data is returned.
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getSmartSelect(function(error, data) {console.log('SMART SELECT is: ' + JSON.stringify(data));}, 'SMART');
// SMART SELECT is: "0"
*/
MarantzDenonTelnet.prototype.getSmartSelect = function(callback, zone) {
var mdt = this;
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MS' : zone;
var regexp = RegExp('(?:^|[\r])' + commandPrefix + 'SMART(.*)');
this.telnet(commandPrefix + 'SMART ?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, ret);
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
}, regexp);
};
/**
Select the SMART SELECT of a zone.
Telnet Command examples: SMART1, SMART2
@param {number} input Supported values: 1 ... 5
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setSmartSelect(1, function(error, data) {console.log('Set SMART SELECT of MAIN ZONE to 1.');});
// Set SMART SELECT of MAIN ZONE to 1.
*/
MarantzDenonTelnet.prototype.setSmartSelect = function(input, callback, zone) {
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MS' : zone;
this.telnet(commandPrefix + 'SMART' + input, function(error, data) {
if (!error) {
callback(null, data);
} else {
callback(error);
}
});
};
/**
Store current setup to SMART SELECT of a zone.
Telnet Command examples: SMART1 MEMORY, SMART2 MEMORY
@param {number} input Supported values: 1 ... 5
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.storeSmartSelect(1, function(error, data) {console.log('Store current settings to SMART SELECT of MAIN ZONE.');});
// Store current settings to SMART SELECT of MAIN ZONE.
*/
MarantzDenonTelnet.prototype.storeSmartSelect = function(input, callback) {
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MS' : zone;
this.telnet(commandPrefix + 'SMART' + input + ' MEMORY', function(error, data) {
if (!error) {
callback(null, data);
} else {
callback(error);
}
});
};
/**
Get the currently selected video source.
Telnet Command examples: SV?
@param {defaultCallback} callback Function to be called when the command is run and data is returned. Will return one or more of:
'DVD', 'BD', 'TV', 'SAT/CBL', 'MPLAY', 'GAME', 'AUX1', 'AUX2', 'AUX3', 'AUX4'
'AUX5', 'AUX6', 'AUX7', 'CD', 'SOURCE', 'ON', 'OFF'
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getVideoSelect(function(error, data) {console.log('VIDEO SELECT of MAIN ZONE is: ' + JSON.stringify(data));}, 'ZM');
// VIDEO SELECT is: "OFF"
*/
MarantzDenonTelnet.prototype.getVideoSelect = function(callback) {
var mdt = this;
var regexp = RegExp('(?:^|[\r])SV(.*)');
this.telnet('SV?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, ret);
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
}, regexp);
};
/**
Select the video source.
Telnet Command examples: SVBD, SVDVD, SVCD
@param {string} input Supported values: 'DVD', 'BD', 'TV', 'SAT/CBL', 'MPLAY',
'GAME', 'AUX1', 'AUX2', 'AUX3', 'AUX4', 'AUX5', 'AUX6', 'AUX7', 'CD', 'SOURCE', 'ON', 'OFF'
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setVideoSelect('MPLAY', function(error, data) {console.log('Set VIDEO SELECT of MAIN ZONE to MPLAY.');});
// Set VIDEO SELECT to MPLAY.
*/
MarantzDenonTelnet.prototype.setVideoSelect = function(input, callback) {
this.telnet('SV' + input, function(error, data) {
if (!error) {
callback(null, data);
} else {
callback(error);
}
});
};
/**
Get the current volume of a zone.
There is no MAIN ZONE Volue, its handled by the Mastervolume (MV)
Telnet Command examples: MV10, Z215
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getVolume(function(error, data) {console.log('VOLUME of MAIN ZONE is: ' + data);}, 'ZM');
// VOLUME of MAIN ZONE is: [0-100]
*/
MarantzDenonTelnet.prototype.getVolume = function(callback, zone) {
var mdt = this;
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MV' : zone;
var regexp = RegExp('(?:^|[\r])' + commandPrefix + '(\\d+)');
this.telnet(commandPrefix + '?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, parseInt((ret + '0').substr(0, 3), 10) * 0.1);
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
}, regexp);
};
/**
Set the playback volume of a zone.
There is no MAIN ZONE Volue, its handled by the Mastervolume (MV)
Telnet Command examples: MV20, Z230, Z340
@param {number} volume 0-100
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {?string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setVolume(30, function(error, data) {console.log('Set VOLUME of MAIN ZONE to 30.');}, 'ZM');
// Set VOLUME of MAIN ZONE to 30.
*/
MarantzDenonTelnet.prototype.setVolume = function(volume, callback, zone) {
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MV' : zone;
var vol = (volume * 10).toFixed(0); //volume fix
if (vol < 100) {
vol = '0' + vol;
} else {
vol = '' + vol;
}
this.telnet(commandPrefix + vol, function(error, data) {
if (!error) {
callback(null, volume);
} else {
callback(error);
}
});
};
/**
Get all supported zones of the AVR.
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getZones(function(error, data) {console.log('Available Zones: ' + JSON.stringify(data));});
// Available Zones: {"ZM":"MAIN ZONE","Z2":"ZONE2","Z3":"ZONE3"}
*/
MarantzDenonTelnet.prototype.getZones = function(callback) {
var mdt = this;
var zoneIds = ['', 'ZM', 'Z2', 'Z3', 'Z4', 'Z5', 'Z6', 'Z7', 'Z8', 'Z9'];
var zones = {};
// get number of zones (PW? -> ["PWON","Z2ON","Z3ON"])
var handleZoneIds = function(err, data) {
var regexp = RegExp(/(Z\d)ON/);
var ret;
if (data) {
for (i = 0; i < data.length; i++) {
if (ret = regexp.exec(data[i])) {
zones[ret[1]] = ret[1];
} else if (data[i] == 'PWON') {
zones['ZM'] = 'ZM';
}
}
}
mdt.telnet('RR?', handleZoneNames);
};
// Try to get zone names supported by recent AVR (RR? -> ["R1MAIN ZONE ","R2ZONE2 ","R3ZONE3"])
var handleZoneNames = function(err, data) {
var regexp = RegExp(/R(\d)(.*)/);
var ret;
var zoneName;
if (data) {
for (i = 0; i < data.length; i++) {
if (ret = regexp.exec(data[i])) {
zoneName = data[i].trim().substr(2);
zones[zoneIds[parseInt(ret[1], 10)]] = zoneName;
}
}
}
callback(null, zones); // whatever happens, we finally go on
};
this.telnet('PW?', handleZoneIds);
};
/**
Returns the current power state of a zone.
Telnet Command examples: PW?, Z2?, Z3?
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.getZonePowerState(function(error, data) {console.log('POWER state of ZONE3 is: ' + (data ? 'ON' : 'OFF'));}, 'Z3');
// POWER state of ZONE3 is: [ON|OFF]
*/
MarantzDenonTelnet.prototype.getZonePowerState = function(callback, zone) {
var mdt = this;
var commandPrefix = (!zone || (zone == 'ZM')) ? 'ZM' : zone;
var regexp = RegExp('(?:^|[\r])' + commandPrefix + '(ON|OFF)');
this.telnet(commandPrefix + '?', function(error, data) {
var ret;
if (!error) {
if (ret = mdt.parseSimpleResponse(data, regexp)) {
callback(null, (ret == 'ON'));
} else {
callback('MarantzDenonTelnet: Did not get RESPONSE in time.');
}
} else {
callback(error);
}
}, regexp);
};
/**
Sets the power state of a zone.
Telnet Command examples: PWON, PWSTANDBY, Z2ON, Z3OFF
@param {boolean} powerState TRUE to power on
@param {defaultCallback} callback Function to be called when the command is run and data is returned
@param {string} zone NULL or ZM for MAIN ZONE, Z2 ... Zn for all others
@example
var mdt = new MarantzDenonTelnet('127.0.0.1');
mdt.setZonePowerState(false, function(error, data) {console.log('Set POWER state of ZONE3 to OFF.');}, 'Z3');
// Set POWER state of ZONE3 to OFF.
*/
MarantzDenonTelnet.prototype.setZonePowerState = function(powerState, callback, zone) {
var commandPrefix = (!zone || (zone == 'ZM')) ? 'MV' : zone;
this.telnet(commandPrefix + (powerState ? 'ON' : 'OFF'), function(error, data) {
if (!error) {
callback(null, powerState);
} else {
callback(error);
}
});
};
/**
Export.
*/
module.exports = MarantzDenonTelnet;