UNPKG

homebridge-denon-heos

Version:

Denon and possibly Marantz (some are tested with positive results) AVR support for Homebridge: https://github.com/nfarina/homebridge with support for newer version of receiver. This plugin uses the http commands to control the receivers. It is also possib

1,702 lines (1,464 loc) 67.7 kB
const request = require('request'); const parseString = require('xml2js').parseString; const telnet = require('telnet-client'); /* Include lib */ const discover = require('./lib/discover'); const pluginName = 'homebridge-denon-heos'; const platformName = 'DenonAVR'; const pluginVersion = '2.9.0'; const defaultPollingInterval = 3; const infoRetDelay = 250; const defaultTrace = false; const autoDiscoverTime = 3000; const setAVRState = false; /* Setup settings button and info button */ const infoMenu = 'MNINF'; const settingsMenu = 'MNMEN ON'; let Service; let Characteristic; let Accessory; let UUIDGen; var traceOn; var debugToInfo; var discoverDev; var g_log; var foundReceivers = []; var cachedAccessories = []; var didFinishLaunching = false; /* Variables for telnet polling system */ var g_powerState = [false,false,false]; var g_volLevel = [0,0,0]; var g_muteState = [false,false,false]; var g_inputID = [null,null,null]; module.exports = (homebridge) => { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; Accessory = homebridge.platformAccessory; UUIDGen = homebridge.hap.uuid; homebridge.registerPlatform(pluginName, platformName, denonClient, true); }; exports.logDebug = function(string) { if (!debugToInfo) g_log.debug(string); else g_log.warn(string); } function logDebug(string) { if (!debugToInfo) g_log.debug(string); else g_log.warn(string); } class denonClient { constructor(log, config, api) { g_log = log; this.port = 3000; this.api = api; /* Stop loading if plugin is not configured */ if (!config || (!Array.isArray(config.devices) && !Array.isArray(config.switches) && !Array.isArray(config.volumeControl))) { g_log.warn("WARNING: No config settings found for homebridge-denon-heos plugin.") return; } traceOn = config.debugTrace || defaultTrace; debugToInfo = config.debugToInfo || false; /* Search for all available Denon receivers */ discoverDev = new discover(this, g_log, foundReceivers, autoDiscoverTime, pluginVersion); api.on('didFinishLaunching', function() { logDebug("DidFinishLaunching"); didFinishLaunching = true; this.configReceivers = []; try { for (let i in config.switches) { if (config.switches[i].ip !== undefined && !this.configReceivers[config.switches[i].ip] ) this.configReceivers[config.switches[i].ip] = new receiver(this, config, config.switches[i].ip); } } catch { g_log.error("ERROR: Could not setup switches") return; } try { for (let i in config.devices) { if (config.devices[i].ip !== undefined && !this.configReceivers[config.devices[i].ip]) this.configReceivers[config.devices[i].ip] = new receiver(this, config, config.devices[i].ip); } } catch { g_log.error("ERROR: Could not setup devices") return; } try { for (let i in config.volumeControl) { if (config.volumeControl[i].ip !== undefined && !this.configReceivers[config.volumeControl[i].ip]){ this.configReceivers[config.volumeControl[i].ip] = new receiver(this, config, config.volumeControl[i].ip);} } } catch { g_log.error("ERROR: Could not setup volume control") return; } setTimeout(this.removeCachedAccessory.bind(this), autoDiscoverTime+5000); }.bind(this)); } configureAccessory(platformAccessory){ if (traceOn) { logDebug('DEBUG: configureAccessory'); try { logDebug(platformAccessory.displayName); } catch { } } cachedAccessories.push(platformAccessory); } removeCachedAccessory(){ if (traceOn) { logDebug('DEBUG: removeCachedAccessory'); try { for (let i in cachedAccessories) logDebug(cachedAccessories[0].displayName); } catch { } } try { this.api.unregisterPlatformAccessories(pluginName, platformName, cachedAccessories); } catch { g_log.error("ERROR: Could not unregister accessories."); } } } class receiver { constructor(base, config, ip) { this.port = 3000; this.api = base.api; this.ip = ip; this.base = base; this.tvAccessories = []; this.legacyAccessories = []; this.volumeAccessories = []; this.switches = config.switches; this.devices = config.devices; this.volumeControl = config.volumeControl; this.devicesDuplicates = []; this.switchesDuplicates = []; this.volumeCtrlDuplicates = []; logDebug('DEBUG: Start receiver with ip: ' + this.ip); this.pollingInterval = config.pollInterval || defaultPollingInterval; this.pollingInterval = this.pollingInterval * 1000; this.htmlControl = true; this.telnetPort = 23; this.devInfoSet = false; this.controlProtocolSet = false; this.telnetConnection; this.manufacturer = 'Denon'; this.modelName = pluginName; this.serialNumber = 'MVV123'; this.firmwareRevision = pluginVersion; this.disableReceiver = false; this.pollingTimeout = false; this.usesManualPort = false; this.webAPIPort = null; this.checkAliveInterval = null; this.zTwoEn = false; this.zThreeEn = false; this.poweredOn = [false,false,false]; this.currentInputID = [null,null,null]; this.volDisp = [null,null,null]; this.volumeLevel = [30,30,30]; this.muteState = [false,false,false]; this.getPortSettings(); // this.startConfiguration(); } getDevInfoSet() { return this.devInfoSet; } setDevInfoSet(set) { this.devInfoSet = set; } returnIP() { return this.ip; } returnPort() { return this.webAPIPort; } hasManualPort() { return this.usesManualPort; } setDisableReceiver(set) { g_log.error('ERROR: Receiver with IP: ' + this.ip + " is disabled. Can't connect through http or Telnet.") this.disableReceiver = set; } getPortSettings() { /* Configure switches */ for (var i in this.switches) { if (this.switches[i].ip === this.ip) { if (this.webAPIPort === null || this.webAPIPort === 'auto') { this.webAPIPort = this.switches[i].port || 'auto'; if(this.webAPIPort != 'auto') this.webAPIPort = this.webAPIPort.toString(); } else { let temp = this.switches[i].port || 'auto'; if(temp != 'auto') { temp = temp.toString(); if (temp != this.webAPIPort) { g_log.error('ERROR: Some manual port number are not equal in config file with receiver: %s', this.ip) } } } } } /* Configure devices */ for (let i in this.devices) { if (this.devices[i].ip === this.ip) { if (this.webAPIPort === null || this.webAPIPort === 'auto') { this.webAPIPort = this.devices[i].port || 'auto'; if(this.webAPIPort != 'auto') this.webAPIPort = this.webAPIPort.toString(); } else { let temp = this.devices[i].port || 'auto'; if(temp != 'auto') { temp = temp.toString(); if (temp != this.webAPIPort) { g_log.error('ERROR: Some manual port number are not equal in config file with receiver: %s', this.ip) } } } } } /* Configure volumeControl */ for (let i in this.volumeControl) { if (this.volumeControl[i].ip === this.ip) { if (this.webAPIPort === null || this.webAPIPort === 'auto') { this.webAPIPort = this.volumeControl[i].port || 'auto'; if(this.webAPIPort != 'auto') this.webAPIPort = this.webAPIPort.toString(); } else { let temp = this.volumeControl[i].port || 'auto'; if(temp != 'auto') { temp = temp.toString(); if (temp != this.webAPIPort) { g_log.error('ERROR: Some manual port number are not equal in config file with receiver: %s', this.ip) } } } } } if(this.webAPIPort === 'auto') { this.discoverControlInterface(); } else if (this.webAPIPort === 'telnet') { this.htmlControl = false; logDebug('DEBUG: Manual control through Telnet set: ' + this.ip); this.controlProtocolSet = true; this.startConfiguration(); } else { this.usesManualPort = true; if(!this.webAPIPort.includes('80')) { g_log.error('ERROR: Current port %s with ip: %s, is not suitable. Use 80 or 8080 manually instead.', this.webAPIPort, this.ip); } logDebug('DEBUG: Manual port ' + this.webAPIPort + ' set: ' + this.ip); this.controlProtocolSet = true; this.startConfiguration(); } } /* * Try to connect through html interface. If not possible go for Telnet. */ discoverControlInterface () { var that = this; /* Try connecting through port 80 */ request('http://' + that.ip + ':80/goform/formMainZone_MainZoneXmlStatusLite.xml', function(error, response, body) { if(error || body.indexOf('Error 403: Forbidden') === 0) { /* Try connecting through port 8080 as 80 could not conenct. */ request('http://' + that.ip + ':8080/goform/formMainZone_MainZoneXmlStatusLite.xml', function(error, response, body) { if(error || body.indexOf('Error 403: Forbidden') === 0) { /* Html control is not possible. Connect through Telnet */ logDebug('DEBUG: No html control possible. Use Telnet control.') that.htmlControl = false; } else { g_log('Using http control on port 8080 with receiver: ' + that.ip); that.webAPIPort = '8080'; that.controlProtocolSet = true; } that.startConfiguration(); }); } else { g_log('Using http control with on 80 with receiver: ' + that.ip); that.webAPIPort = '80'; that.controlProtocolSet = true; that.startConfiguration(); } }); } /* * Try configure the devices. Wait until receiver discovery is finished. */ startConfiguration () { if (this.disableReceiver) return; if (!discoverDev.setDenonInformation(this, g_log) || !didFinishLaunching) { setTimeout(this.startConfiguration.bind(this), infoRetDelay); return; } if (!this.htmlControl) this.connectTelnet(); /* Configure switches */ for (var i in this.switches) { if (this.switches[i].ip === this.ip) { try { if (this.switchesDuplicates[this.switches[i].name]) { g_log.warn("WARNING: A Switch with the name: %s and ip: %s is already added. It will be ignored.", this.switches[i].name, this.switches[i].ip); continue; } else { this.switchesDuplicates[this.switches[i].name] = true; this.legacyAccessories.push(new legacyClient(this, this.switches[i])); } } catch { g_log.error("ERROR: Could not add Switch accessory."); } } } /* Configure devices */ for (let i in this.devices) { if (this.devices[i].ip === this.ip) { try { if (this.devicesDuplicates[this.devices[i].name]) { g_log.warn("WARNING: A Device with the name: %s and ip: %s is already added. It will be ignored.", this.devices[i].name, this.devices[i].ip); continue; } else { this.devicesDuplicates[this.devices[i].name] = true; this.tvAccessories.push(new tvClient(this, this.devices[i])); } } catch { g_log.error("ERROR: Could not add TV accessory."); } } } /* Configure volumeControl */ for (let i in this.volumeControl) { if (this.volumeControl[i].ip === this.ip) { try { if (this.volumeCtrlDuplicates[this.volumeControl[i].name]) { g_log.warn("WARNING: A Volume control with the name: %s and ip: %s is already added. It will be ignored.", this.volumeControl[i].name, this.volumeControl[i].ip); continue; } else { this.volumeCtrlDuplicates[this.volumeControl[i].name] = true; this.volumeAccessories.push(new volumeClient(this, this.volumeControl[i])); } } catch { g_log.error("ERROR: Could not add Volume control accessory."); } } } /* start the polling */ setTimeout(this.startPolling, Math.random() * 3000, this); } /* * Diverted start of polling loop. */ startPolling (that) { if (!that.checkAliveInterval) { that.checkAliveInterval = setInterval(that.pollForUpdates.bind(that, 1), that.pollingInterval); } } /* * This will start a polling loop that goes on forever and updates * the on characteristic periodically. */ pollForUpdates(zone) { if (!this.controlProtocolSet) return; // if (traceOn) // logDebug('DEBUG: pollForUpdates zone: ' + zone + ': ' + this.ip); /* Make sure that no poll is happening just after switch in input/power */ if (this.pollingTimeout) { this.pollingTimeout = false; return; } var that = this; if (this.htmlControl) { var requestString if (zone == 1) requestString = 'http://' + that.ip + ':' + this.webAPIPort + '/goform/formMainZone_MainZoneXmlStatusLite.xml'; else if (zone == 2 || zone == 3) requestString = 'http://' + that.ip + ':' + this.webAPIPort + '/goform/formZone' + zone + '_Zone' + zone + 'XmlStatusLite.xml'; request(requestString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.webAPIPort); logDebug('DEBUG: ' + error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else { parseString(body, function (err, result) { if(err) { logDebug("DEBUG: Error while parsing pollForUpdates. " + err); } else { try { if (that.volDisp[zone] === null) that.volDisp[zone] = result.item.VolumeDisplay[0].value[0]; } catch(error) { that.volDisp[zone] = 'Absolute'; } let powerState = false; try { if (result.item.Power[0].value[0] === 'ON' ) powerState = true; } catch(error) { g_log.error('ERROR: Could not retrieve powerstate. Might be due to a wrong ip address.'); return; } /* Parse volume of receiver to 0-100% */ let volLevel; try { if ( that.volDisp[zone] === 'Absolute' ) { volLevel = parseInt(result.item.MasterVolume[0].value[0]); volLevel = volLevel + 80; } } catch(error) { g_log.error('ERROR: Could not retrieve volume level. Might be due to a wrong ip address.'); return; } /* Parse mutestate receiver to bool of HB */ let muteState = false; try { if (result.item.Mute[0].value[0] === 'on' ) muteState = true; } catch(error) { g_log.error('ERROR: Could not retrieve mute state. Might be due to a wrong ip address.'); return; } let stateInfo = { zone: zone, power: powerState, inputID: result.item.InputFuncSelect[0].value[0], masterVol: volLevel, mute: muteState } if (!that.pollingTimeout) that.updateStates(that, stateInfo, null); if (zone == 1 && that.zTwoEn) that.pollForUpdates(2); else if (zone == 2 && that.zThreeEn) that.pollForUpdates(3); } }); } }); } else { that.telnetConnection.send('PW?'); this.telnetConnection.send('MU?'); this.telnetConnection.send('MV?'); this.telnetConnection.send('SI?'); if (that.zTwoEn) { this.telnetConnection.send('Z2MU?'); that.telnetConnection.send('Z2?'); } if (that.zThreeEn) { this.telnetConnection.send('Z3MU?'); that.telnetConnection.send('Z3?'); } } } /* * Used to update the state of all. Disable polling for one poll. */ updateStates(that, stateInfo, curName) { if (curName) that.pollingTimeout = true; if (stateInfo.masterVol == undefined) stateInfo.masterVol = 0; if (traceOn) logDebug(stateInfo); if (stateInfo.power === true || stateInfo.power === false) that.poweredOn[stateInfo.zone-1] = stateInfo.power; if (stateInfo.inputID) that.currentInputID[stateInfo.zone-1] = stateInfo.inputID; if (stateInfo.mute === true || stateInfo.mute === false) that.muteState[stateInfo.zone-1] = stateInfo.mute; if (stateInfo.masterVol) that.volumeLevel[stateInfo.zone-1] = stateInfo.masterVol; for (let i in that.legacyAccessories) { if (that.legacyAccessories[i].getName() != curName) { that.legacyAccessories[i].setReceiverState(stateInfo); } } for (let i in that.tvAccessories) { if (that.tvAccessories[i].getName() != curName) { that.tvAccessories[i].setReceiverState(stateInfo); } } for (let i in that.volumeAccessories) { if (that.volumeAccessories[i].getName() != curName) { that.volumeAccessories[i].setReceiverState(stateInfo); } } } /* * Setup Telnet connection if no HTML control is possible. */ connectTelnet() { this.telnetConnection = new telnet(); this.telnetConnection.on('connect', () => { this.connected = true; logDebug('DEBUG" connected to ' + this.ip); }); this.telnetConnection.on('close', () => { this.connected = false; logDebug('DEBUG: lost connection to ' + this.ip); if (this.attempts > 5){ g_log.Error("ERROR: Can't connect to receiver with IP: " + this.ip); } setTimeout(() => { connect(); }, 2000); }); this.telnetConnection.on('error', err => { // the close event will be called, too g_log.error(err); }); this.telnetConnection.on('failedLogin', () => { g_log.error("ERROR: Can't login at receiver with IP: " + this.ip); }); this.telnetConnection.on('data', data => this.telnetResponseHandler(data.toString('utf8').replace(/\r?\n|\r/gm, '')) ); connect(this); } send(cmd) { if (cmd && this.queue.length) { logDebug('DEBUG: pushed command to queue'); this.queue.push(cmd); return; } if (!cmd && this.queue.length) { logDebug('DEBUG: get cmd from queue'); cmd = this.queue[0]; this.queue.shift(); } this.telnetConnection .send(cmd + '\r') .then(() => { logDebug('DEBUG: command ' + cmd + ' successfully send'); if (this.queue.length) setTimeout(() => this.send(), 100); }) .catch(err => { logDebug('DEBUG: error: ' + err); if (this.queue.length) setTimeout(() => this.send(), 100); }); } telnetResponseHandler(res) { const regExp = /MUON|MUOFF|PWON|PWSTANDBY|MV\d{1,3}|SI\S{1,10}|Z2MUON|Z2MUOFF|Z2ON|Z2OFF|Z2\d{1,3}|Z2\S{1,10}|Z3MUON|Z3MUOFF|Z3ON|Z3OFF|Z3\d{1,3}|Z3\S{1,10}/gm; res = res.match(regExp); if (!Array.isArray(res)) return; for (let i = 0; i < res.length; i++) { // logDebug('DEBUG: received response: ' + res[i]); switch (res[i].slice(0,2)) { case 'PW': if (res[i] === 'PWON') g_powerState[0] = true; else if (res[i] === 'PWSTANDBY') g_powerState[0] = false; break; case 'MV': if (res[i] === /MV\d{1,3}/g.exec(res[i])[0]) { g_volLevel[0] = parseInt(/\d{1,3}/g.exec(res[i])[0]); } break; case 'MU': if (res[i] === 'MUON') g_muteState[0] = true; else if (res[i] === 'MUOFF') g_muteState[0] = false; break; case 'SI': if (res[i].slice(0,7) === 'SINFAIS') { break; } if (res[i] === /SI\S{1,20}/g.exec(res[i])[0]) { let stateInfo = { zone: 1, power: g_powerState[0], inputID: res[i].slice(2), masterVol: g_volLevel[0], mute: g_muteState[0] } if (!this.pollingTimeout) this.updateStates(this, stateInfo, null); } break; case 'Z2': if (res[i] === 'Z2ON') g_powerState[1] = true; else if (res[i] === 'Z2OFF') g_powerState[1] = false; else if (res[i] === 'Z2MUON') g_muteState[1] = true; else if (res[i] === 'Z2MUOFF') g_muteState[1] = false; else if (res[i].slice(2,9) === 'SINFAIS') break; else if (parseInt(res[i].slice(2)) == parseInt(res[i].slice(2))) { let stateInfo = { zone: 2, power: g_powerState[1], inputID: g_inputID[1], masterVol: parseInt(res[i].slice(2,5)), mute: g_muteState[1] } if (!this.pollingTimeout) this.updateStates(this, stateInfo, null); } else if (res[i] === /Z2\S{1,20}/g.exec(res[i])[0]) g_inputID[1] = res[i].slice(2); break; case 'Z3': if (res[i] === 'Z3ON') g_powerState[2] = true; else if (res[i] === 'Z3OFF') g_powerState[2] = false; else if (res[i] === 'Z3MUON') g_muteState[2] = true; else if (res[i] === 'Z3MUOFF') g_muteState[2] = false; else if (res[i].slice(2,9) === 'SINFAIS') break; else if (parseInt(res[i].slice(2)) == parseInt(res[i].slice(2))) { let stateInfo = { zone: 2, power: g_powerState[2], inputID: g_inputID[2], masterVol: parseInt(res[i].slice(2,5)), mute: g_muteState[2] } if (!this.pollingTimeout) this.updateStates(this, stateInfo, null); } else if (res[i] === /Z3\S{1,20}/g.exec(res[i])[0]) g_inputID[2] = res[i].slice(2); break; } } } } connect = async (that) => { that.attempts++; const params = { host: that.ip, port: 23, echoLines: 0, irs: '\r', negotiationMandatory: false, ors: '\r\n', separator: false, shellPrompt: '', timeout: 800, }; await that.telnetConnection.connect(params); that.connected = true; that.attempts = 0; that.controlProtocolSet = true; g_log('Using Telnet control with receiver: ' + that.ip); }; class tvClient { constructor(recv, device) { this.port = 3000; this.api = recv.api; this.recv = recv; this.tvServicePort = recv.webAPIPort; this.manufacturer = recv.manufacturer; this.modelName = recv.modelName; this.serialNumber = recv.serialNumber; this.firmwareRevision = recv.firmwareRevision; // configuration this.name = device.name || 'Denon Receiver'; this.ip = device.ip; this.inputs = device.inputs; this.zone = device.zone || 1; if (this.zone < 1 || this.zone > 3) this.zone = 1; if (this.zone == 2) this.recv.zTwoEn = true; if (this.zone == 3) this.recv.zThreeEn = true; this.iterator = this.zone - 1; this.defaultVolume = {}; this.switchInfoMenu = device.switchInfoMenu; if (this.switchInfoMenu === true) { this.infoButton = settingsMenu; this.menuButton = infoMenu; } else { this.infoButton = infoMenu; this.menuButton = settingsMenu; } this.defaultInputID = device.defaultInputID; /* setup variables */ this.inputIDSet = false; this.inputIDs = new Array(); this.setDefaultInputTimeout; /* Delay to wait for retrieve device info */ this.setupTvService(); } /***************************************** * Start of TV integration service ****************************************/ setupTvService() { if (traceOn) logDebug('DEBUG: setupTvService zone: ' + this.zone + ': ' + this.name); this.tvAccesory = new Accessory(this.name, UUIDGen.generate(this.ip+this.name+"tvService")); this.tvAccesory.category = this.api.hap.Categories.AUDIO_RECEIVER; this.tvService = new Service.Television(this.name, 'tvService'); this.tvService .setCharacteristic(Characteristic.ConfiguredName, this.name); this.tvService .setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE); this.tvService .getCharacteristic(Characteristic.Active) .on('get', this.getPowerState.bind(this)) .on('set', this.setPowerState.bind(this)); this.tvService .getCharacteristic(Characteristic.ActiveIdentifier) .on('set', (inputIdentifier, callback) => { this.setAppSwitchState(true, callback, this.inputIDs[inputIdentifier]); }) .on('get', this.getAppSwitchState.bind(this)); this.tvService .getCharacteristic(Characteristic.RemoteKey) .on('set', this.remoteKeyPress.bind(this)); this.tvService .getCharacteristic(Characteristic.PowerModeSelection) .on('set', (newValue, callback) => { if (this.recv.poweredOn[this.iterator]) { request('http://' + this.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + this.menuButton, function(error, response, body) {}); } callback(); }); this.tvAccesory .getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) .setCharacteristic(Characteristic.Model, this.modelName) .setCharacteristic(Characteristic.SerialNumber, this.serialNumber) .setCharacteristic(Characteristic.FirmwareRevision, this.firmwareRevision); this.tvAccesory.addService(this.tvService); this.setupTvSpeakerService(); this.setupInputSourcesService(); logDebug('DEBUG: publishExternalAccessories zone: ' + this.zone + ': ' + this.name); this.api.publishExternalAccessories(pluginName, [this.tvAccesory]); } setupTvSpeakerService() { if (traceOn) logDebug('DEBUG: setupTvSpeakerService zone: ' + this.zone + ': ' + this.name); this.tvSpeakerService = new Service.TelevisionSpeaker(this.name + ' Volume', 'tvSpeakerService'); this.tvSpeakerService .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); this.tvSpeakerService .getCharacteristic(Characteristic.VolumeSelector) .on('set', (state, callback) => { logDebug('DEBUG: Remote control (VolumeSelector), pressed: ' + state === 1 ? 'Down' : 'Up'); this.setVolumeSwitch(state, callback, !state); }); this.tvSpeakerService .getCharacteristic(Characteristic.Volume) .on('get', this.getVolume.bind(this)) .on('set', this.setVolume.bind(this)); this.tvSpeakerService .getCharacteristic(Characteristic.Mute) .on('get', this.getMuteState.bind(this)) .on('set', this.setMuteState.bind(this)); this.tvAccesory.addService(this.tvSpeakerService); this.tvService.addLinkedService(this.tvSpeakerService); } setupInputSourcesService() { if (traceOn) logDebug('DEBUG: setupInputSourcesService zone: ' + this.zone + ': ' + this.name); if (this.inputs === undefined || this.inputs === null || this.inputs.length <= 0) { return; } if (Array.isArray(this.inputs) === false) { this.inputs = [this.inputs]; } let savedNames = {}; this.inputs.forEach((value, i) => { // get inputID let inputID = null; if (value.inputID !== undefined) { inputID = value.inputID; } else { inputID = value; } // get name let inputName = inputID; if (savedNames && savedNames[inputID]) { inputName = savedNames[inputID]; } else if (value.name) { inputName = value.name; } this.defaultVolume[inputID] = 0 if (value.defaultVolume !== undefined) { this.defaultVolume[inputID] = value.defaultVolume; } // if inputID not null or empty add the input if (inputID !== undefined && inputID !== null && inputID !== '') { inputID = inputID.replace(/\s/g, ''); // remove all white spaces from the string let tempInput = new Service.InputSource(inputID, 'inputSource' + i); tempInput .setCharacteristic(Characteristic.Identifier, i) .setCharacteristic(Characteristic.ConfiguredName, inputName) .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION) .setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN); tempInput .getCharacteristic(Characteristic.ConfiguredName) .on('set', (name, callback) => { savedNames[inputID] = name; callback() }); this.tvAccesory.addService(tempInput); if (!tempInput.linked) this.tvService.addLinkedService(tempInput); this.inputIDs.push(inputID); } }); } /***************************************** * End of TV integration service ****************************************/ /***************************************** * Start of helper methods ****************************************/ updateReceiverState(tvStatus) { if (!tvStatus) { if (this.powerService) this.powerService .getCharacteristic(Characteristic.On) .updateValue(false); if (this.tvService) this.tvService .getCharacteristic(Characteristic.Active) .updateValue(false); //tv service if (this.volumeService) { this.volumeService .getCharacteristic(Characteristic.On) .updateValue(false); this.volumeService .getCharacteristic(Characteristic.Volume) .updateValue(this.recv.volumeLevel[this.iterator]); this.volumeService .getCharacteristic(Characteristic.Mute) .updateValue(false); } } else { if (this.powerService) this.powerService .getCharacteristic(Characteristic.On) .updateValue(true); if (this.tvService) this.tvService .getCharacteristic(Characteristic.Active) .updateValue(true); //tv service if (this.volumeService) { this.volumeService .getCharacteristic(Characteristic.On) .updateValue(true); this.volumeService .getCharacteristic(Characteristic.Volume) .updateValue(this.recv.volumeLevel[this.iterator]); this.volumeService .getCharacteristic(Characteristic.Mute) .updateValue(this.recv.muteState[this.iterator]); } } } setReceiverState(stateInfo) { if (this.zone != stateInfo.zone) return; if (traceOn && setAVRState) logDebug('DEBUG: setReceiverState zone: ' + this.zone + ': ' + this.name); if (stateInfo.power === true || stateInfo.power === false) this.updateReceiverState(this.recv.poweredOn[this.iterator]); if (stateInfo.inputID) { if (this.recv.poweredOn[this.iterator]) { let inputName = stateInfo.inputID; for (let i = 0; i < this.inputIDs.length; i++) { if (inputName === this.inputIDs[i]) { if (this.inputIDSet === false) this.tvService .getCharacteristic(Characteristic.ActiveIdentifier) .updateValue(i); else this.inputIDSet = false; } } } } } /***************************************** * End of helper methods ****************************************/ /***************************************** * Start of Homebridge Setters/Getters ****************************************/ getPowerState(callback) { if (traceOn) logDebug('DEBUG: getPowerState zone: ' + this.zone + ': ' + this.name); callback(null, this.recv.poweredOn[this.iterator] ? 1 : 0); } setPowerState(state, callback) { if (traceOn) logDebug('DEBUG: setPowerState zone: ' + this.zone + 'state: ' + this.name); if (state === 0) state = false; else if (state === 1) state = true; if (this.recv.htmlControl) { var that = this; var stateString = (state ? 'On' : 'Standby'); request('http://' + that.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppPower.xml?' + that.zone + '+Power' + stateString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.tvServicePort); logDebug('DEBUG: ' + error); callback(error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else { if (state && that.defaultInputID != undefined && that.defaultInputID != "None") { that.setDefaultInputTimeout = setTimeout(function(){ that.setAppSwitchState(state, callback, that.defaultInputID); }, 8000); } else { clearTimeout(that.setDefaultInputTimeout); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: state, inputID: null, masterVol: null, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } } }); } else { var stateString; if (this.zone == 1) stateString = 'PW' + (state ? 'ON' : 'STANDBY'); else if (this.zone == 2 || this.zone == 3) stateString = 'Z' + this.zone + (state ? 'ON' : 'OFF'); this.recv.telnetConnection.send(stateString); /* Update possible other switches and accessories too */ let stateInfo = { zone: this.zone, power: state, inputID: null, masterVol: null, mute: null } this.recv.updateStates(this.recv, stateInfo, this.name); callback(); } } // getVolumeSwitch(callback) { // if (traceOn) // logDebug('DEBUG: getVolumeSwitch: ' + this.name); // callback(null, false); // } setVolumeSwitch(state, callback, isUp) { if (traceOn) logDebug('DEBUG: setVolumeSwitch zone: ' + this.zone + ': ' + this.name); var zoneString; if (this.zone == 1) zoneString = 'MV'; else if (this.zone == 2 || this.zone == 3) zoneString = 'Z' + this.zone; var that = this; if (this.recv.poweredOn[this.iterator]) { var stateString = zoneString + (isUp ? 'UP' : 'DOWN'); if (this.recv.htmlControl) { request('http://' + this.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + stateString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.tvServicePort); logDebug('DEBUG: ' + error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } }); } else { that.recv.telnetConnection.send(stateString); } } callback(); } getAppSwitchState(callback) { if (traceOn) logDebug('DEBUG: getAppSwitchState zone: ' + this.zone); if (this.recv.poweredOn[this.iterator]) { let inputName = this.recv.currentInputID[this.iterator]; for (let i = 0; i < this.inputIDs.length; i++) { if (inputName === this.inputIDs[i]) { this.tvService .getCharacteristic(Characteristic.ActiveIdentifier) .updateValue(i); callback(null, i); return; } } } callback(null, 0); } setAppSwitchState(state, callback, inputName) { if (traceOn) logDebug('DEBUG: setAppSwitchState zone: ' + this.zone + ': ' + this.name); this.inputIDSet = true; var level = this.defaultVolume[inputName]; var inputString; var volumeString; let inputNameN = inputName.replace('/', '%2F'); if (this.zone == 1) { inputString = 'SI' + inputNameN; volumeString = 'MV'; } else if (this.zone == 2 || this.zone == 3) { inputString = 'Z' + this.zone + inputNameN; volumeString = 'Z' + this.zone; } var that = this; if (this.recv.htmlControl) { request('http://' + that.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + inputString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.tvServicePort); logDebug('DEBUG: ' + error); if (callback) callback(error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else { /* Set default volume if this is set */ if (level > 0) that.setDefaultVolume(that, level, volumeString); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: that.recv.poweredOn[that.iterator], inputID: inputName, masterVol: null, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } }); } else { that.recv.telnetConnection.send(inputString); /* Set default volume if this is set */ if (level > 0) that.setDefaultVolume(that, level, volumeString); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: that.recv.poweredOn[that.iterator], inputID: inputName, masterVol: null, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } } getMuteState(callback) { if (traceOn) logDebug('DEBUG: getMuteState: ' + this.name); if (this.recv.poweredOn[this.iterator]) { callback(null, this.recv.muteState[this.iterator]); } else { callback(null, false); } } setMuteState(state, callback) { if (traceOn) logDebug('DEBUG: setMuteState zone: ' + this.zone + ': ' + this.name); var stateString; if (this.zone == 1) stateString = (state ? 'MUON' : 'MUOFF'); else if (this.zone == 2 || this.zone == 3) stateString = 'Z' + this.zone + 'MU' + (state ? 'ON' : 'OFF'); var that = this; if (this.recv.htmlControl) { request('http://' + this.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + stateString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.tvServicePort); logDebug('DEBUG: ' + error); callback(error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else { let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: null, mute: state } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } }); } else { that.recv.telnetConnection.send(stateString); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: null, mute: state } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } } getVolume(callback) { if (traceOn) logDebug('DEBUG: getVolume zone: ' + this.zone + ': ' + this.name); if (this.recv.poweredOn[this.iterator]) { callback(null, this.recv.volumeLevel[this.iterator]); } else { callback(null, 0); } } setVolume(level, callback) { if (traceOn) logDebug('DEBUG: setVolume: ' + this.name + ' to :' + level); this.recv.volumeLevel[this.iterator] = level; var that = this; var denonLevel; if (level < 10) denonLevel = '0' + level.toString(); else denonLevel = level.toString(); var zoneSettings; if (this.zone == 1) zoneSettings = 'MV'; else if (this.zone == 2 || this.zone == 3) zoneSettings = 'Z' + this.zone; if (this.recv.htmlControl) { request('http://' + that.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + zoneSettings + denonLevel, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.tvServicePort); logDebug('DEBUG: ' + error); callback(error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else { /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: level, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } }); } else { that.recv.telnetConnection.send(zoneSettings + denonLevel); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: level, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); callback(); } } setDefaultVolume(that, level, volumeString) { if (level < 10) volumeString = volumeString + '0' + level.toString(); else volumeString = volumeString + level.toString(); /* Set default volume if this is set */ if (that.recv.htmlControl) { setTimeout(function(){ request('http://' + that.ip + ':' + that.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + volumeString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.legacyPort); logDebug('DEBUG: ' + error); callback(error); } else { /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: parseInt(level), mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); } }); }, 2000); } else { if (level > 0) { setTimeout(function(){ that.recv.telnetConnection.send(volumeString); /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: null, inputID: null, masterVol: parseInt(level), mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); }, 2000); } } } remoteKeyPress(remoteKey, callback) { if (traceOn) logDebug('DEBUG: Denon - remote key pressed: ' + remoteKey); var ctrlString = ''; switch (remoteKey) { case Characteristic.RemoteKey.REWIND: ctrlString = 'MN9E'; break; case Characteristic.RemoteKey.FAST_FORWARD: ctrlString = 'MN9D'; break; case Characteristic.RemoteKey.NEXT_TRACK: ctrlString = 'MN9F'; break; case Characteristic.RemoteKey.PREVIOUS_TRACK: ctrlString = 'MN9G'; break; case Characteristic.RemoteKey.ARROW_UP: ctrlString = 'MNCUP'; break; case Characteristic.RemoteKey.ARROW_DOWN: ctrlString = 'MNCDN'; break; case Characteristic.RemoteKey.ARROW_LEFT: ctrlString = 'MNCLT'; break; case Characteristic.RemoteKey.ARROW_RIGHT: ctrlString = 'MNCRT'; break; case Characteristic.RemoteKey.SELECT: ctrlString = 'MNENT'; break; case Characteristic.RemoteKey.BACK: ctrlString = 'MNRTN'; break; case Characteristic.RemoteKey.EXIT: ctrlString = 'MNRTN'; break; case Characteristic.RemoteKey.PLAY_PAUSE: ctrlString = 'NS94'; break; case Characteristic.RemoteKey.INFORMATION: ctrlString = this.infoButton; break; } if (this.recv.poweredOn[this.iterator]) { if (this.recv.htmlControl) request('http://' + this.ip + ':' + this.tvServicePort + '/goform/formiPhoneAppDirect.xml?' + ctrlString, function(error, response, body) {}); else this.recv.telnetConnection.send(ctrlString); } callback(); } getName() { return this.name; } /***************************************** * End of Homebridge Setters/Getters ****************************************/ } class legacyClient { constructor(recv, switches) { this.port = 3000; this.api = recv.api; this.recv = recv; this.legacyPort = recv.webAPIPort; this.manufacturer = recv.manufacturer; this.modelName = recv.modelName; this.serialNumber = recv.serialNumber; this.firmwareRevision = recv.firmwareRevision; // configuration this.name = switches.name || 'Denon Input'; this.ip = switches.ip; this.inputID = switches.inputID; this.zone = switches.zone || 1; if (this.zone < 1 || this.zone > 3) this.zone = 1; if (this.zone == 2) this.recv.zTwoEn = true; if (this.zone == 3) this.recv.zThreeEn = true; this.iterator = this.zone - 1; this.pollAllInput = switches.pollAllInput || false; this.level = switches.defaultVolume; this.denonLevel; if (this.level > 0) { if (this.level < 10) this.denonLevel = '0' + this.level.toString(); else this.denonLevel = this.level.toString(); } /* setup variables */ this.switchState = false; this.setupLegacyService(); } /***************************************** * Start of legacy service ****************************************/ setupLegacyService() { if (traceOn) logDebug('DEBUG: setupLegacyService zone: ' + this.zone + ': ' + this.name); /* Delay to wait for retrieve device info */ this.uuid = UUIDGen.generate(this.name+this.ip+toString(this.zone)+"switch"); let isCached = this.testCachedAccessories(); if (!isCached) { g_log("New switch configured: " + this.name); this.accessory = new Accessory(this.name, this.uuid); this.switchService = new Service.Switch(this.name, 'legacyInput'); this.switchService .getCharacteristic(Characteristic.On) .on('get', this.getPowerStateLegacy.bind(this)) .on('set', this.setPowerStateLegacy.bind(this)); this.accessory .getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, this.manufacturer) .setCharacteristic(Characteristic.Model, this.modelName) .setCharacteristic(Characteristic.SerialNumber, this.serialNumber) .setCharacteristic(Characteristic.FirmwareRevision, this.firmwareRevision); this.accessory.addService(this.switchService); try { this.api.registerPlatformAccessories(pluginName, platformName, [this.accessory]); } catch { g_log.error("ERROR: Could not register switch with name: %s", this.accessory.context.name); } } else { g_log("Configured switch found: " + this.name); this.accessory .getService(Service.Switch) .getCharacteristic(Characteristic.On) .on('get', this.getPowerStateLegacy.bind(this)) .on('set', this.setPowerStateLegacy.bind(this)); // this.api.updatePlatformAccessories([this.accessory]); } } setReceiverState(stateInfo) { if (this.zone != stateInfo.zone) return; if (traceOn && setAVRState) logDebug('DEBUG: setReceiverState zone: ' + this.zone + ': ' + this.name); if ((stateInfo.power === true || stateInfo.power === false) && stateInfo.inputID) { if (stateInfo.power && (this.pollAllInput || stateInfo.inputID === this.inputID)) { this.switchState = true; } else { this.switchState = false; } if (this.accessory) { this.accessory .getService(Service.Switch) .getCharacteristic(Characteristic.On) .updateValue(this.switchState); } } else if ((stateInfo.power === true || stateInfo.power === false) && !stateInfo.inputID) { if (stateInfo.power && this.pollAllInput) { this.switchState = true; } else { this.switchState = false; } if (this.accessory) { this.accessory .getService(Service.Switch) .getCharacteristic(Characteristic.On) .updateValue(this.switchState); } } } getPowerStateLegacy(callback) { if (traceOn) logDebug('DEBUG: getPowerStateLegacy zone: ' + this.zone + ': ' + this.name); let switchState = false; if (this.recv.poweredOn[this.iterator] && (this.recv.currentInputID[this.iterator] == this.inputID || this.pollAllInput)) switchState = true callback(null, switchState); } setPowerStateLegacy(state, callback) { if (traceOn) logDebug('DEBUG: setPowerStateLegacy zone: ' + this.zone + ' state: ' + this.name); if (this.recv.htmlControl) { this.setPowerStateHTML(state, callback); } else { this.setPowerStateTelNet(state, callback); } } setPowerStateHTML(state, callback) { var stateString = (state ? 'On' : 'Standby'); var inputString; let inputNameN = this.inputID.replace('/', '%2F'); if (this.zone == 1) { inputString = 'SI' + inputNameN; } else if (this.zone == 2 || this.zone == 3) { inputString = 'Z' + this.zone + inputNameN; } var that = this; if (this.recv.poweredOn[this.iterator] != state) { request('http://' + that.ip + ':' + that.legacyPort + '/goform/formiPhoneAppPower.xml?' + that.zone + '+Power' + stateString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.legacyPort); logDebug('DEBUG: ' + error); callback(error); } else if (body.indexOf('Error 403: Forbidden') === 0) { g_log.error('ERROR: Can not access receiver with IP: %s. Might be due to a wrong port. Try 80 or 8080 manually in config file.', that.ip); } else if(state) { /* Switch to correct input if switching on and legacy service */ request('http://' + that.ip + ':' + that.legacyPort + '/goform/formiPhoneAppDirect.xml?' + inputString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.legacyPort); logDebug('DEBUG: ' + error); callback(error); } else { /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: state, inputID: that.inputID, masterVol: null, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); that.setDefaultVolume(that); callback(); } }); } else { /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: state, inputID: that.inputID, masterVol: null, mute: null } that.recv.updateStates(that.recv, stateInfo, that.name); that.setDefaultVolume(that); callback(); } }); } else { request('http://' + that.ip + ':' + that.legacyPort + '/goform/formiPhoneAppDirect.xml?' + inputString, function(error, response, body) { if(error) { g_log.error("ERROR: Can't connect to receiver with ip: %s and port: %s", that.ip, that.legacyPort); logDebug('DEBUG: ' + error); callback(error); } else { /* Update possible other switches and accessories too */ let stateInfo = { zone: that.zone, power: state, inputID: