homebridge-denon-tv
Version:
Homebridge plugin to control Denon/Marantz AV Receivers.
127 lines (110 loc) • 4.63 kB
JavaScript
import axios from 'axios';
import { Agent as HttpsAgent } from 'https';
import EventEmitter from 'events';
import ImpulseGenerator from './impulsegenerator.js';
import Functions from './functions.js';
import { XMLParser } from 'fast-xml-parser';
import { ApiUrls, ManufacturerMap } from './constants.js';
class Denon extends EventEmitter {
constructor(config, devInfoFile) {
super();
this.host = config.host;
this.generation = config.generation || 0;
this.logDebug = config.log?.debug || false;
this.devInfoFile = devInfoFile;
this.firstRun = true;
this.functions = new Functions();
const baseUrl = `http://${config.host}:${config.port}`;
const commonConfig = {
baseURL: baseUrl,
timeout: 20000
};
const httpsConfig = this.generation === 2 ? { httpsAgent: new HttpsAgent({ rejectUnauthorized: false, keepAlive: false }) } : {};
this.client = axios.create({
...commonConfig,
...httpsConfig,
});
const options = {
ignoreAttributes: false,
ignorePiTags: true,
allowBooleanAttributes: true
};
this.parseString = new XMLParser(options);
// execution queue (prevents race conditions and lost ticks)
this.executionQueue = Promise.resolve();
const runQueued = (task) => {
this.executionQueue = this.executionQueue
.then(() => task())
.catch((error) => {
this.emit('error', error?.message ?? error);
});
return this.executionQueue;
};
this.impulseGenerator = new ImpulseGenerator()
.on('connect', () =>
runQueued(async () => {
await this.connect();
})
)
.on('checkState', () =>
runQueued(async () => {
this.emit('checkState');
})
)
.on('state', (state) => {
this.emit(state ? 'success' : 'warn', `Impulse generator ${state ? 'started' : 'stopped'}`);
});
}
async connect() {
try {
// Fetch & parse denon info
const deviceInfoUrl = [ApiUrls.DeviceInfoGen0, ApiUrls.DeviceInfoGen1, ApiUrls.DeviceInfoGen2][this.generation];
const deviceInfo = await this.client.get(deviceInfoUrl);
const parseData = this.parseString.parse(deviceInfo.data);
const generationMap = {
0: parseData.item,
1: parseData.Device_Info,
2: parseData.Device_Info
};
const denonInfo = generationMap[this.generation];
if (this.logDebug) this.emit('debug', `Connect data: ${JSON.stringify(denonInfo, null, 2)}`);
// Device info
denonInfo.info = {
manufacturer: ManufacturerMap[denonInfo.BrandCode ?? 2],
modelName: denonInfo.ModelName || denonInfo.FriendlyName?.value || 'AV Receiver',
serialNumber: denonInfo.MacAddress?.toString() || `1234567654321${this.host}`,
firmwareRevision: denonInfo.UpgradeVersion?.toString() || '00',
deviceZones: denonInfo.DeviceZones ?? 1,
apiVersion: denonInfo.CommApiVers || '000'
};
// Capabilities
const caps = denonInfo.DeviceCapabilities || {};
const setup = caps.Setup || {};
denonInfo.info.supportPictureMode = setup.PictureMode?.Control === 1;
denonInfo.info.supportSoundMode = setup.SoundMode?.Control === 1;
denonInfo.info.supportFavorites = caps.Operation?.Favorites?.Control === 1;
// Success event
if (this.firstRun) {
this.emit('success', `Connect Success`);
await this.functions.saveData(this.devInfoFile, denonInfo.info);
this.firstRun = false;
}
// Emit denon info
this.emit('denonInfo', denonInfo);
return denonInfo;
} catch (error) {
throw new Error(`Connect error: ${error}`);
}
}
async send(command) {
try {
const path = `${ApiUrls.iPhoneDirect}${command}`;
await this.client.get(path);
if (this.logDebug) this.emit('debug', `Send path: ${path}`);
return true;
} catch (error) {
throw new Error(`Send data error: ${error}`);
}
}
}
export default Denon;