homebridge-generic-avr
Version:
Homebridge plugin for AV Receivers. Support Onyko, Denon AVR
1,187 lines (1,026 loc) • 40.1 kB
JavaScript
;
let Service, Characteristic, Accessory, UUID;
const PLUGIN_NAME = 'homebridge-generic-avr'
const pollingtoevent = require('polling-to-event');
const info = require('./package.json');
const importFresh = require('import-fresh')
class WrappingLog {
static get LOG_TYPES() {
return ["error", "info", "debug"];
}
constructor(prefix, log) {
this.prefix = prefix;
this.log = log;
WrappingLog.LOG_TYPES.forEach(type => {
const callback = log[type].bind(log);
this[type] = (first, ...rest) => callback(this.prefix + first, ...rest);
});
}
}
class GenericAvrPlatform {
constructor(log, config, api) {
var that = this;
that.reachable = false
this.api = api;
this.config = config;
this.log = log;
this.receivers = [];
this.receiverAccessories = [];
this.connections = {};
this.bridged = config.bridged || false;
this.debugverbose = config.debugverbose || false;
this.log.info('**************************************************************')
this.log.info(' ' + PLUGIN_NAME + ' version ' + info.version)
this.log.info(' GitHub: https://github.com/hlyi/' + PLUGIN_NAME )
this.log.info('**************************************************************')
this.log.info('Use bridge mode: ' + this.bridged);
this.log.info('start success...');
this.log.debug('Debug mode enabled');
if (typeof config.receivers === 'undefined' ) {
this.log.error('ERROR: your configuration is incorrect. Configuration changed with version 0.7.x');
this.receivers = [];
}else{
config['receivers'].forEach ( (receiver, i) => {
let recvidx = i+1
if ( typeof receiver.vendor === 'undefined') {
this.log.error('Missing vendor in receiver ' + recvidx + '\'s configuration')
}else if ( receiver.vendor in vendors ){
if ( typeof receiver.name === 'undefined' ) {
this.log.error('Missing name in receiver ' + recvidx + '\'s configuration');
}else if ( typeof receiver.ip_address === 'undefined' ) {
this.log.error('Missing IP address in receiver ' + recvidx + '\'s configuration');
}else {
this.receivers.push(receiver)
}
}else {
this.log.error('Unsupported vendor : ' + receiver.vendor + ' in receiver ' + recvidx + '\'s configuration')
}
});
if ( ! this.bridged ) {
this.createAccessories()
}
}
}
accessories (callback) {
if ( this.bridged ) {
this.createAccessories()
callback(this.foundReceivers)
}else {
callback ( [] )
}
}
createAccessories () {
this.foundReceivers = []
var numReceivers = this.receivers.length
this.log.info('Adding %s AVRs', numReceivers)
this.receivers.forEach ( device => {
try {
this.log.info( 'Adding Receiver: ' + device.name )
const accessory = new GenericAvrAccessory(this, device)
this.log.info( accessory.name + ' is added')
this.foundReceivers.push(accessory)
}
catch ( e ) {
this.log.error( "Can't create AVR : " + device.name + " at " + device.ip_address + " with error: " + e.message)
}
})
this.log.info('Added %s AVRs', this.foundReceivers.length)
}
}
class GenericAvrAccessory {
constructor(platform, receiver) {
var that = this
this.platform = platform;
this.bridged = platform.bridged;
this.setAttempt = 0;
this.config = receiver;
this.debugverbose = this.config.debugverbose || platform.debugverbose;
this.name = this.config.name;
this.log = new WrappingLog(`(${this.name}) `,platform.log);
this.ip_address = this.config.ip_address;
if ( receiver.model.startsWith ( receiver.vendor + ' ' ) ){
this.model = receiver.model.substr(receiver.vendor.length + 1);
}else {
throw new Error('Unsupported %s Model: %s', receiver.vendor, receiver.model );
}
this.zone = (this.config.zone || 'main').toLowerCase();
if (typeof this.config.volume_dimmer === 'undefined') {
this.log.error('ERROR: Your configuration is missing the parameter "volume_dimmer". Assuming "false".');
this.volume_dimmer = false;
} else {
this.volume_dimmer = this.config.volume_dimmer;
}
if (typeof this.config.filter_inputs === 'undefined') {
this.log.error('ERROR: Your configuration is missing the parameter "filter_inputs". Assuming "false".');
this.filter_inputs = false;
} else {
this.filter_inputs = this.config.filter_inputs;
}
this.inputs = this.config.inputs;
this.poll_status_interval = this.config.poll_status_interval || '0';
this.defaultInput = this.config.default_input;
this.defaultVolume = this.config.default_volume;
this.maxVolume = this.config.max_volume || 60;
this.mapVolume100 = this.config.map_volume_100 || true;
this.state = false;
this.m_state = false;
this.v_state = 0;
this.i_state = null;
this.interval = Number.parseInt(this.poll_status_interval, 10);
this.avrManufacturer = this.config.vendor;
this.avrSerial = this.config.serial || this.ip_address;
this.switchHandling = 'check';
if (this.interval > 10 && this.interval < 100000)
this.switchHandling = 'poll';
this.vendor = new vendors[this.config.vendor](this);
if ( ! this.vendor.createRxInput(this) ) {
throw new Error('Unsupported %s Model: %s', this.config.vendor, this.model );
}
this.log.debug('name: %s', this.name);
this.log.debug('IP: %s', this.ip_address);
this.log.debug('Model: %s', this.model);
this.log.debug('Zone: %s', this.zone);
this.log.debug('volume_dimmer: %s', this.volume_dimmer);
this.log.debug('filter_inputs: %s', this.filter_inputs);
this.log.debug('poll_status_interval: %s', this.poll_status_interval);
this.log.debug('defaultInput: %s', this.defaultInput);
this.log.debug('defaultVolume: %s', this.defaultVolume);
this.log.debug('maxVolume: %s', this.maxVolume);
this.log.debug('mapVolume100: %s', this.mapVolume100);
this.log.debug('avrSerial: %s', this.avrSerial);
// Option to only configure specified inputs with filter_inputs
if (this.filter_inputs) {
// Check the RxInputs.Inputs items to see if each exists in this.inputs. Return new array of those that do.
this.RxInputs.Inputs = this.RxInputs.Inputs.filter(rxinput => {
return that.inputs.some(input => {
return input.input_name === rxinput.label;
});
});
}
if ( ! this.bridged ) {
this.rcvuuid = UUID.generate(this.constructor.name + this.name + this.ip_address )
this.acc = new Accessory(this.name, this.rcvuuid, this.platform.api.hap.Accessory.Categories.TELEVISION);
this.acc.removeService(this.acc.getService(Service.AccessoryInformation));
this.createServices().forEach(service => { this.acc.addService(service) });
}
// this.createRxInput();
this.vendor.connectAvr(this);
this.polling(this);
if ( ! this.bridged ) {
this.platform.api.publishExternalAccessories(PLUGIN_NAME, [this.acc]);
}
}
getServices () {
if ( this.bridged ) return this.createServices()
}
createServices () {
var that = this
let services = []
// add basic information
let infoAcc = new Service.AccessoryInformation;
infoAcc
.setCharacteristic(Characteristic.Manufacturer, this.avrManufacturer)
.setCharacteristic(Characteristic.Model, this.model)
.setCharacteristic(Characteristic.SerialNumber, this.avrSerial)
.setCharacteristic(Characteristic.FirmwareRevision, info.version)
.setCharacteristic(Characteristic.Name, this.name);
services.push( infoAcc );
// add tv service
this.log.debug('Creating TV service for receiver %s', this.name);
this.tvService = new Service.Television( this.name, 'tvService');
this.tvService
.getCharacteristic(Characteristic.ConfiguredName)
.setValue(this.name)
.setProps({
perms: [Characteristic.Perms.READ]
});
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', this.setInputSource.bind(this))
.on('get', this.getInputSource.bind(this));
this.tvService
.getCharacteristic(Characteristic.RemoteKey)
.on('set', this.remoteKeyPress.bind(this));
services.push( this.tvService)
// add tv speaker
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', this.setVolumeRelative.bind(this));
this.tvSpeakerService
.getCharacteristic(Characteristic.Mute)
.on('get', this.getMuteState.bind(this))
.on('set', this.setMuteState.bind(this));
this.tvSpeakerService
.addCharacteristic(Characteristic.Volume)
.on('get', this.getVolumeState.bind(this))
.on('set', this.setVolumeState.bind(this));
this.tvService.addLinkedService(this.tvSpeakerService);
services.push(this.tvSpeakerService)
// input selector
// Create final array of inputs, using any labels defined in the config's inputs to override the default labels
this.RxInputs.Inputs.forEach((i, index) => {
let inputName = i.label;
if (that.inputs) {
that.inputs.forEach(input => {
if (input.input_name === i.label)
inputName = input.display_name;
});
}
let input = new Service.InputSource( that.name + ' ' + inputName, i.code);
input
.setCharacteristic(Characteristic.Identifier, index + 1 )
.setCharacteristic(Characteristic.ConfiguredName, inputName)
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED)
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI)
.getCharacteristic(Characteristic.ConfiguredName).setProps({ perms: [Characteristic.Perms.READ] });
that.tvService.addLinkedService(input);
services.push(input)
});
if (this.volume_dimmer) {
this.log.debug('Creating Dimmer service linked to TV for receiver %s', this.name);
this.dimmer = new Service.Lightbulb( this.name + ' Volume', 'dimmer');
this.dimmer
.getCharacteristic(Characteristic.On)
.on('get', callback => {
this.getMuteState((error, value) => {
if (error) {
callback(error);
return;
}
callback(null, !value);
});
})
.on('set', (value, callback) => this.setMuteState(!value, callback));
this.dimmer
.addCharacteristic(Characteristic.Brightness)
.on('get', this.getVolumeState.bind(this))
.on('set', this.setVolumeState.bind(this));
this.tvSpeakerService.addLinkedService(this.dimmer);
}
return services
}
polling(platform) {
const that = platform;
// Status Polling
if (that.switchHandling === 'poll') {
// somebody instroduced powerurl but we are never using it.
// const powerurl = that.status_url;
that.log.debug('start long poller..');
// PWR Polling
const statusemitter = pollingtoevent(done => {
that.log.debug('start PWR polling..');
that.getPowerState((error, response) => {
// pass also the setAttempt, to force a homekit update if needed
done(error, response, that.setAttempt);
}, 'statuspoll');
}, {longpolling: true, interval: that.interval * 1000, longpollEventName: 'statuspoll'});
statusemitter.on('statuspoll', data => {
that.state = data;
that.log.debug('event - PWR status poller - new state: ', that.state);
// if (that.tvService ) {
// that.tvService.getCharacteristic(Characteristic.Active).updateValue(that.state, null, 'statuspoll');
// }
});
// Audio-Input Polling
const i_statusemitter = pollingtoevent(done => {
that.log.debug('start INPUT polling..');
that.getInputSource((error, response) => {
// pass also the setAttempt, to force a homekit update if needed
done(error, response, that.setAttempt);
}, 'i_statuspoll');
}, {longpolling: true, interval: that.interval * 1000, longpollEventName: 'i_statuspoll'});
i_statusemitter.on('i_statuspoll', data => {
that.i_state = data;
that.log.debug('event - INPUT status poller - new i_state: ', that.i_state);
// if (that.tvService ) {
// that.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(that.i_state, null, 'i_statuspoll');
// }
});
// Audio-Muting Polling
const m_statusemitter = pollingtoevent(done => {
that.log.debug('start MUTE polling..');
that.getMuteState((error, response) => {
// pass also the setAttempt, to force a homekit update if needed
done(error, response, that.setAttempt);
}, 'm_statuspoll');
}, {longpolling: true, interval: that.interval * 1000, longpollEventName: 'm_statuspoll'});
m_statusemitter.on('m_statuspoll', data => {
that.m_state = data;
that.log.debug('event - MUTE status poller - new m_state: ', that.m_state);
// if (that.tvService ) {
// that.tvService.getCharacteristic(Characteristic.Mute).updateValue(that.m_state, null, 'm_statuspoll');
// }
});
// Volume Polling
const v_statusemitter = pollingtoevent(done => {
that.log.debug('start VOLUME polling..');
that.getVolumeState((error, response) => {
// pass also the setAttempt, to force a homekit update if needed
done(error, response, that.setAttempt);
}, 'v_statuspoll');
}, {longpolling: true, interval: that.interval * 1000, longpollEventName: 'v_statuspoll'});
v_statusemitter.on('v_statuspoll', data => {
that.v_state = data;
that.log.debug('event - VOLUME status poller - new v_state: ', that.v_state);
// if (that.tvService ) {
// that.tvService.getCharacteristic(Characteristic.Volume).updateValue(that.v_state, null, 'v_statuspoll');
// }
});
}
}
/// ////////////////
// EVENT FUNCTIONS
/// ////////////////
eventDebug(response) {
this.log.debug('eventDebug: %s', response);
}
eventError(response) {
this.log.error('eventError: %s', response);
}
eventConnect(response) {
this.log.debug('eventConnect: %s', response);
this.reachable = true;
}
eventSystemPower(setOn) {
if (this.state !== setOn)
this.log.info('Event - System Power changed: %s', setOn);
this.state = setOn
this.log.debug('eventSystemPower - message: %s, new state %s', setOn, this.state);
// Communicate status
if (this.tvService)
this.tvService.getCharacteristic(Characteristic.Active).updateValue(this.state);
// if (this.volume_dimmer) {
// this.m_state = !(response == 'on');
// this.dimmer.getCharacteristic(Characteristic.On).updateValue((response == 'on'), null, 'power event m_status');
// }
}
eventAudioMuting(response) {
this.m_state = (response === 'on');
this.log.debug('eventAudioMuting - message: %s, new m_state %s', response, this.m_state);
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Mute).updateValue(this.m_state, null, 'm_statuspoll');
}
eventInput(label) {
if (label) {
// Convert to i_state input code
const index =
label !== null ? // eslint-disable-line no-negated-condition
this.RxInputs.Inputs.findIndex(i => i.label === label ) :
-1;
if (this.i_state !== (index + 1))
this.log.info('Event - Input changed: %s', label);
this.i_state = index + 1;
this.log.debug('eventInput - new i_state: %s - input: %s', this.i_state, label);
// this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(this.i_state);
} else {
// Then invalid Input chosen
this.log.error('eventInput - ERROR - INVALID INPUT - Model does not support selected input.');
}
// Communicate status
if (this.tvService)
this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(this.i_state);
}
eventVolume(response) {
if (this.mapVolume100) {
const volumeMultiplier = this.maxVolume / 100;
const newVolume = response / volumeMultiplier;
this.v_state = Math.round(newVolume);
this.log.debug('eventVolume - message: %s, new v_state %s PERCENT', response, this.v_state);
} else {
this.v_state = response;
this.log.debug('eventVolume - message: %s, new v_state %s ACTUAL', response, this.v_state);
}
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Volume).updateValue(this.v_state, null, 'v_statuspoll');
}
eventClose(response) {
this.log.debug('eventClose: %s', response);
this.reachable = false;
}
/// /////////////////////
// GET AND SET FUNCTIONS
/// /////////////////////
setPowerState(powerOn, callback, context) {
let that = this
// if context is statuspoll, then we need to ensure that we do not set the actual value
if (context && context === 'statuspoll') {
this.log.debug('setPowerState - polling mode, ignore, state: %s', this.state);
callback(null, this.state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.setAttempt++;
// do the callback immediately, to free homekit
// have the event later on execute changes
this.state = powerOn;
callback();
if (powerOn) {
this.log.debug('setPowerState - actual mode, power state: %s, switching to ON', this.state);
this.vendor.setPowerStateOn( error => {
// this.log.debug( 'PWR ON: %s - %s -- current state: %s', error, response, this.state);
if (error) {
this.state = false;
this.log.error('setPowerState - PWR ON: ERROR - current state: %s', this.state);
// if (this.tvService ) {
// this.tvService.getCharacteristic(Characteristic.Active).updateValue(powerOn, null, 'statuspoll');
// }
} else {
// If the AVR has just been turned on, apply the default volume
this.log.debug('Attempting to set the default volume to ' + this.defaultVolume);
if (powerOn && this.defaultVolume) {
this.log.info('Setting default volume to ' + this.defaultVolume);
this.vendor.setVolumeState(this.defaultVolume, error=>{
if (error)
this.log.error('Error while setting default volume: %s', error);
});
}
// If the AVR has just been turned on, apply the Input default
this.log.debug('Attempting to set the default input selector to ' + this.defaultInput);
// Handle defaultInput being either a custom label or manufacturer label
let label = this.defaultInput;
if (this.inputs) {
this.inputs.forEach(input => {
if (input.input_name === this.default)
label = input.input_name;
else if (input.display_name === this.defaultInput)
label = input.display_name;
});
}
const index =
label !== null ? // eslint-disable-line no-negated-condition
that.RxInputs.Inputs.findIndex(i => i.label === label) :
-1;
this.i_state = index + 1;
if (powerOn && label) {
this.log.info('Setting default input selector to ' + label);
this.vendor.setInputSource ( label, error =>{
if (error)
this.log.error('Error while setting default input: %s', error);
});
}
}
});
} else {
this.log.debug('setPowerState - actual mode, power state: %s, switching to OFF', this.state);
this.vendor.setPowerStateOff( error => {
// this.log.debug( 'PWR OFF: %s - %s -- current state: %s', error, response, this.state);
if (error) {
this.state = false;
this.log.error('setPowerState - PWR OFF: ERROR - current state: %s', this.state);
// if (this.tvService ) {
// this.tvService.getCharacteristic(Characteristic.Active).updateValue(this.state, null, 'statuspoll');
// }
}
});
}
// if (this.volume_dimmer) {
// this.m_state = !(powerOn == 'on');
// this.dimmer.getCharacteristic(Characteristic.On).updateValue((powerOn == 'on'), null, 'power event m_status');
// }
this.tvService.getCharacteristic(Characteristic.Active).updateValue(this.state);
}
getPowerState(callback, context) {
// if context is statuspoll, then we need to request the actual value
if ((!context || context !== 'statuspoll') && this.switchHandling === 'poll') {
this.log.debug('getPowerState - polling mode, return state: ', this.state);
callback(null, this.state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, this.state);
this.log.debug('getPowerState - actual mode, return state: ', this.state);
this.vendor.getPowerState( error => {
if (error) {
this.state = false;
this.log.debug('getPowerState - PWR QRY: ERROR - current state: %s', this.state);
}
});
this.tvService.getCharacteristic(Characteristic.Active).updateValue(this.state);
}
getVolumeState(callback, context) {
// if context is v_statuspoll, then we need to request the actual value
if ((!context || context !== 'v_statuspoll') && this.switchHandling === 'poll') {
this.log.debug('getVolumeState - polling mode, return v_state: ', this.v_state);
callback(null, this.v_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, this.v_state);
this.log.debug('getVolumeState - actual mode, return v_state: ', this.v_state);
this.vendor.getVolumeState ( error => {
if (error) {
this.v_state = 0;
this.log.debug('getVolumeState - VOLUME QRY: ERROR - current v_state: %s', this.v_state);
}
});
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Volume).updateValue(this.v_state);
}
setVolumeState(volumeLvl, callback, context) {
// if context is v_statuspoll, then we need to ensure this we do not set the actual value
if (context && context === 'v_statuspoll') {
this.log.debug('setVolumeState - polling mode, ignore, v_state: %s', this.v_state);
callback(null, this.v_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.setAttempt++;
// Are we mapping volume to 100%?
if (this.mapVolume100) {
const volumeMultiplier = this.maxVolume / 100;
const newVolume = volumeMultiplier * volumeLvl;
this.v_state = Math.round(newVolume);
this.log.debug('setVolumeState - actual mode, PERCENT, volume v_state: %s', this.v_state);
} else if (volumeLvl > this.maxVolume) {
// Determin if maxVolume threshold breached, if so set to max.
this.v_state = this.maxVolume;
this.log.debug('setVolumeState - VOLUME LEVEL of: %s exceeds maxVolume: %s. Resetting to max.', volumeLvl, this.maxVolume);
} else {
// Must be using actual volume number
this.v_state = volumeLvl;
this.log.debug('setVolumeState - actual mode, ACTUAL volume v_state: %s', this.v_state);
}
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, this.v_state);
this.vendor.setVolumeState ( this.v_state, error =>{
if (error) {
this.v_state = 0;
this.log.debug('setVolumeState - VOLUME : ERROR - current v_state: %s', this.v_state);
}
});
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Volume).updateValue(this.v_state);
}
setVolumeRelative(volumeDirection, callback, context) {
// if context is v_statuspoll, then we need to ensure this we do not set the actual value
if (context && context === 'v_statuspoll') {
this.log.debug('setVolumeRelative - polling mode, ignore, v_state: %s', this.v_state);
callback(null, this.v_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.setAttempt++;
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, this.v_state);
if (volumeDirection === Characteristic.VolumeSelector.INCREMENT) {
this.log.debug('setVolumeRelative - VOLUME : level-up');
this.vendor.setVolumeRelative(true, error => {
if (error) {
this.v_state = 0;
this.log.error('setVolumeRelative - VOLUME : ERROR - current v_state: %s', this.v_state);
}
});
} else if (volumeDirection === Characteristic.VolumeSelector.DECREMENT) {
this.log.debug('setVolumeRelative - VOLUME : level-down');
this.vendor.setVolumeRelative(false, error => {
if (error) {
this.v_state = 0;
this.log.error('setVolumeRelative - VOLUME : ERROR - current v_state: %s', this.v_state);
}
});
} else {
this.log.error('setVolumeRelative - VOLUME : ERROR - unknown direction sent');
}
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Volume).updateValue(this.v_state);
}
getMuteState(callback, context) {
// if context is m_statuspoll, then we need to request the actual value
if ((!context || context !== 'm_statuspoll') && this.switchHandling === 'poll') {
this.log.debug('getMuteState - polling mode, return m_state: ', this.m_state);
callback(null, this.m_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, this.m_state);
this.log.debug('getMuteState - actual mode, return m_state: ', this.m_state);
this.vendor.getMuteState(error=>{
if (error) {
this.m_state = false;
this.log.debug('getMuteState - MUTE QRY: ERROR - current m_state: %s', this.m_state);
}
});
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Mute).updateValue(this.m_state);
}
setMuteState(muteOn, callback, context) {
// if context is m_statuspoll, then we need to ensure this we do not set the actual value
if (context && context === 'm_statuspoll') {
this.log.debug('setMuteState - polling mode, ignore, m_state: %s', this.m_state);
callback(null, this.m_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.setAttempt++;
// do the callback immediately, to free homekit
// have the event later on execute changes
this.m_state = muteOn;
callback(null, this.m_state);
if (this.m_state) {
this.log.debug('setMuteState - actual mode, mute m_state: %s, switching to ON', this.m_state);
this.vendor.setMuteState(true, error=> {
if (error) {
this.m_state = false;
this.log.error('setMuteState - MUTE ON: ERROR - current m_state: %s', this.m_state);
}
});
} else {
this.log.debug('setMuteState - actual mode, mute m_state: %s, switching to OFF', this.m_state);
this.vendor.setMuteState(false, error=> {
if (error) {
this.m_state = false;
this.log.error('setMuteState - MUTE OFF: ERROR - current m_state: %s', this.m_state);
}
});
}
// Communicate status
if (this.tvSpeakerService)
this.tvSpeakerService.getCharacteristic(Characteristic.Mute).updateValue(this.m_state);
}
getInputSource(callback, context) {
// if context is i_statuspoll, then we need to request the actual value
if ((!context || context !== 'i_statuspoll') && this.switchHandling === 'poll') {
this.log.debug('getInputState - polling mode, return i_state: ', this.i_state);
callback(null, this.i_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.log.debug('getInputState - actual mode, return i_state: ', this.i_state);
this.vendor.getInputSource(error =>{
if (error) {
this.i_state = 1;
this.log.error( 'getInputState - INPUT QRY: ERROR - current i_state: ' + this.i_state);
this.log.error(error);
callback(error);
return;
}
});
callback(null, this.i_state === null ? 0 : this.i_state);
// Communicate status
if (this.tvService && this.i_state !== null )
this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(this.i_state);
}
setInputSource(source, callback, context) {
// if context is i_statuspoll, then we need to ensure this we do not set the actual value
if (context && context === 'i_statuspoll') {
this.log.info('setInputState - polling mode, ignore, i_state: %s', this.i_state);
callback(null, this.i_state);
return;
}
if (!this.ip_address) {
this.log.error('Ignoring request; No ip_address defined.');
callback(new Error('No ip_address defined.'));
return;
}
this.setAttempt++;
this.i_state = source;
const label = this.RxInputs.Inputs[this.i_state - 1].label;
this.log.debug('setInputState - actual mode, ACTUAL input i_state: %s - label: %s', this.i_state, label);
// do the callback immediately, to free homekit
// have the event later on execute changes
//FIXME callback(null, this.i_state);
callback();
this.vendor.setInputSource(label, error => {
if (error)
this.log.error('setInputState - INPUT : ERROR - current i_state:%s - Source:%s', this.i_state, source.toString());
});
// Communicate status
if (this.tvService)
this.tvService.getCharacteristic(Characteristic.ActiveIdentifier).updateValue(this.i_state);
}
remoteKeyPress(button, callback) {
// do the callback immediately, to free homekit
// have the event later on execute changes
callback(null, button);
if (this.buttons[button]) {
const press = this.buttons[button];
this.log.debug('remoteKeyPress - INPUT: pressing key %s', press);
this.vendor.remoteKeyPress( press, error => {
if (error) {
// this.i_state = 1;
this.log.error('remoteKeyPress - INPUT: ERROR pressing button %s', press);
}
});
} else {
this.log.error('Remote button %d not supported.', button);
}
}
identify(callback) {
this.log.info('Identify requested! %s', this.ip_address);
callback(); // success
}
}
// add protocol and comdev
class OnkyoAvrAccessory {
constructor (obj ) {
this.zone = obj.zone;
this.log = obj.log;
this.debugverbose = obj.debugverbose;
this.cmdMap = new Array(2);
this.cmdMap.main = new Array(4);
this.cmdMap.main.power = 'system-power';
this.cmdMap.main.volume = 'master-volume';
this.cmdMap.main.muting = 'audio-muting';
this.cmdMap.main.input = 'input-selector';
this.cmdMap.zone2 = new Array(4);
this.cmdMap.zone2.power = 'power';
this.cmdMap.zone2.volume = 'volume';
this.cmdMap.zone2.muting = 'muting';
this.cmdMap.zone2.input = 'selector';
obj.maxVolume = obj.config.max_volume || 60;
obj.zone = (obj.config.zone || 'main').toLowerCase();
if ( obj.zone !== 'main' && obj.zone !== 'zone2' ) {
throw new Error('Unsupported zone: ' + obj.config.zone);
}
obj.buttons = {
[Characteristic.RemoteKey.REWIND]: 'rew',
[Characteristic.RemoteKey.FAST_FORWARD]: 'ff',
[Characteristic.RemoteKey.NEXT_TRACK]: 'skip-f',
[Characteristic.RemoteKey.PREVIOUS_TRACK]: 'skip-r',
[Characteristic.RemoteKey.ARROW_UP]: 'up', // 4
[Characteristic.RemoteKey.ARROW_DOWN]: 'down', // 5
[Characteristic.RemoteKey.ARROW_LEFT]: 'left', // 6
[Characteristic.RemoteKey.ARROW_RIGHT]: 'right', // 7
[Characteristic.RemoteKey.SELECT]: 'enter', // 8
[Characteristic.RemoteKey.BACK]: 'exit', // 9
[Characteristic.RemoteKey.EXIT]: 'exit', // 10
[Characteristic.RemoteKey.PLAY_PAUSE]: 'play', // 11
[Characteristic.RemoteKey.INFORMATION]: 'home' // 15
};
}
connectAvr( obj ) {
this.eiscp = importFresh('eiscp');
this.log.debug("Connecting to " + obj.name + " with IP: " + obj.ip_address);
this.eiscp.connect ( { host: obj.ip_address, reconnect: true, model: obj.model} );
// bind callback
if ( this.debugverbose ) {
this.eiscp.on('debug', obj.eventDebug.bind(obj));
}
this.eiscp.on('error', obj.eventError.bind(obj));
this.eiscp.on('connect', obj.eventConnect.bind(obj));
this.eiscp.on('close', obj.eventClose.bind(obj));
this.eiscp.on(this.cmdMap[this.zone].power, response => obj.eventSystemPower(this.isSystemPowerOn(response)));
this.eiscp.on(this.cmdMap[this.zone].volume, obj.eventVolume.bind(obj));
this.eiscp.on(this.cmdMap[this.zone].muting, obj.eventAudioMuting.bind(obj));
this.eiscp.on(this.cmdMap[this.zone].input, response => obj.eventInput(this.eventInputLabel(response)));
}
createRxInput (obj ) {
obj.log.debug("Creating RX input");
// Create the RxInput object for later use.
const eiscpDataAll = require('eiscp/eiscp-commands.json');
const inSets = [];
let set;
for (set in eiscpDataAll.modelsets) {
eiscpDataAll.modelsets[set].forEach(model => {
if (model.includes(obj.model))
inSets.push(set);
});
}
if ( inSets.length < 1 ) {
obj.log.error('Can not find model ' + obj.model +' in the database');
return false
}
// Get list of commands from eiscpData
const eiscpData = eiscpDataAll.commands.main.SLI.values;
// Create a JSON object for inputs from the eiscpData
let newobj = '{ "Inputs" : [';
let exkey;
for (exkey in eiscpData) {
let hold = eiscpData[exkey].name.toString();
if (hold.includes(','))
hold = hold.slice(0, hold.indexOf(','));
if (exkey.includes('“') || exkey.includes('”')) {
exkey = exkey.replace(/“/g, '');
exkey = exkey.replace(/”/g, '');
}
if (exkey.includes('UP') || exkey.includes('DOWN') || exkey.includes('QSTN'))
continue;
// Work around specific bug for “26”
if (exkey === '“26”')
exkey = '26';
if (exkey in eiscpData) {
if ('models' in eiscpData[exkey])
set = eiscpData[exkey].models;
else
continue;
} else {
continue;
}
if (inSets.includes(set))
newobj = newobj + '{ "code":"' + exkey + '" , "label":"' + hold + '" },';
else
continue;
}
// Drop last comma first
newobj = newobj.slice(0, -1) + ']}';
obj.log.debug(newobj);
obj.RxInputs = JSON.parse(newobj);
return true
}
isSystemPowerOn(msg) {
return msg === 'on'
}
eventInputLabel (msg ) {
let label = JSON.stringify(msg);
label = label.replace(/[[\]"]+/g, '');
if (label.includes(','))
label = label.slice(0, label.indexOf(','));
this.log.debug('eventInput - message: %s - input: %s', msg, label);
return label;
}
setPowerStateOn(callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].power + '=on', callback)
}
setPowerStateOff(callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].power + '=standby', callback)
}
getPowerState(callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].power + '=query', callback)
}
getVolumeState(callback ) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].volume + '=query', callback )
}
setVolumeState(volumeLvl, callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].volume + ':' + volumeLvl, callback )
}
setVolumeRelative(volUp, callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].volume + ':level-' + (volUp ? 'up' : 'down' ), callback)
}
getMuteState(callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].muting + '=query', callback)
}
setMuteState(mute, callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].muting + '=' + (mute ? 'on' :'off'), callback)
}
getInputSource(callback) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].input + '=query', callback)
}
setInputSource(label, callback ) {
this.eiscp.command(this.zone + '.' + this.cmdMap[this.zone].input + ':' + label, callback)
}
remoteKeyPress(press, callback ) {
this.eiscp.command(this.zone + '.setup=' + press, callback)
}
}
const DenonAvrTelnet = require('denon-avr-telnet')
class DenonAvrAccessory {
constructor (obj ) {
this.zone = obj.zone;
this.log = obj.log;
this.debugverbose = obj.debugverbose;
obj.maxVolume = obj.config.max_volume || 99;
obj.zone = (obj.config.zone || 'MAIN').toUpperCase().replace('MAIN','MZ').replace('ZONE','Z');
if (obj.zone !== 'MZ' && ! obj.zone.match('^Z\\d$') ) {
throw new Error ( 'Unsupported zone: ' + obj.config.zone );
}
// FIXME
obj.buttons = {
};
}
connectAvr( obj ) {
this.log.debug("Connecting to " + obj.name + " with IP: " + obj.ip_address);
this.denonClient = new DenonAvrTelnet.DenonAvrTelnet(obj.ip_address, { timeout:2000, sendTimeout:2400} );
// bind callback
this.denonClient.on('error', obj.eventError.bind(obj));
if ( this.debugverbose ) {
this.denonClient.on('raw', obj.eventDebug.bind(obj));
this.denonClient.on('send', obj.eventDebug.bind(obj));
}
this.denonClient.on('connected', ()=> obj.eventConnect("Denon connected"));
this.denonClient.on('close', obj.eventClose.bind(obj));
this.denonClient.on('powerChanged', obj.eventSystemPower.bind(obj));
this.denonClient.on('volumeChanged', vol => obj.eventVolume (this.normalizeVolume(vol)));
this.denonClient.on('muteChanged', obj.eventAudioMuting.bind(obj));
this.denonClient.on('inputChanged', obj.eventInput.bind(obj));
}
createRxInput (obj ) {
obj.log.debug("Creating RX input");
// Create the RxInput object for later use.
const inSets = DenonAvrTelnet.SI_TYPES;
let newobj = '{ "Inputs" : [';
inSets.forEach ( (i, idx ) => {
newobj += idx ? ', ' : '';
newobj += '{ "code":"' + i + '", "label":"' + i + '" }';
});
newobj += ']}';
// Drop last comma first
obj.log.debug(newobj);
obj.RxInputs = JSON.parse(newobj);
return true
}
normalizeVolume (vol){
return Math.ceil(vol+81)
}
setPowerStateOn(callback) {
// this.log.debug('Denon set Power On' )
this.denonClient.setPower(true).catch(callback)
}
setPowerStateOff(callback) {
// this.log.debug('Denon set Power Off' )
this.denonClient.setPower(false).catch(callback)
}
getPowerState(callback) {
// this.log.debug('Denon get Power' )
this.denonClient.getPower().catch(callback)
}
getVolumeState(callback ) {
// this.log.debug('Denon get Volume' )
this.denonClient.getVolume().catch(callback)
}
setVolumeState(volumeLvl, callback) {
// this.log.debug('Denon set Volume to ' + volumeLvl)
this.denonClient.setVolume(volumeLvl).catch(callback)
}
setVolumeRelative(volUp, callback) {
// this.log.debug('Denon set Volume %s', volUp ? "Up" : "Down")
this.denonClient.setVolumeRelative(volUp).catch(callback)
}
getMuteState(callback) {
// this.log.debug('Denon get Mute' )
this.denonClient.getMute().catch(callback)
}
setMuteState(mute, callback) {
// this.log.debug('Denon set Mute %s', mute ? "On" : "Off")
this.denonClient.setMute(mute).catch(callback)
}
getInputSource(callback) {
// this.log.debug('Denon get Input Source' )
this.denonClient.getInput().catch(callback)
}
setInputSource(label, callback ) {
// this.log.debug('Denon set Input to ' + label)
this.denonClient.setInput(label).catch(callback)
}
remoteKeyPress(press, callback ) {
this.log.debug('Denon set Remote to ' + press)
callback();
}
}
const vendors = {
"Onkyo" : OnkyoAvrAccessory,
"Marantz" : DenonAvrAccessory,
"Denon" : DenonAvrAccessory
}
module.exports = (api) => {
Accessory = api.platformAccessory;
Characteristic = api.hap.Characteristic;
Service = api.hap.Service;
UUID = api.hap.uuid;
api.registerPlatform(PLUGIN_NAME, 'GenericAvr', GenericAvrPlatform);
};