homebridge-denon-v3
Version:
Denon and Marantz AVR support for Homebridge: https://github.com/nfarina/homebridge
253 lines • 8.54 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DenonMarantzController = void 0;
const telnet_client_1 = __importDefault(require("telnet-client"));
const types_1 = require("./types");
function msleep(n) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, n);
}
const serverControllerPort = 23;
class DenonMarantzController {
constructor(log, ipaddress) {
this.COMMANDS = {
"PW": ["Power", null],
"ZM": ["Main Zone", null],
"Z2": ["Zone 2", (data) => this.parseZ2(data)],
"Z3": ["Zone 3", (data) => this.parseZ3(data)],
"MU": ["Muted", null],
"Z2MU": ["Z2 Muted", null],
"Z3MU": ["Z3 Muted", null],
"MV": ["Volume", (data) => this.parseMV(data)],
"Z2MV": ["Z2 Volume", null],
"Z3MV": ["Z3 Volume", null],
"SI": ["Source", null],
"Z2SI": ["Z2 Source", null],
"Z3SI": ["Z3 Source", null],
// "MS": [ "Surround Mode", null],
// "CV": [ "Channel Bias", null],
// "PV": [ "Picture Mode", null],
// "ECO": [ "Eco Mode", null],
// "SSSOD": [ "Available Source", null],
// "PSDEL": [ "Sound Delay", null],
// "PSDRC": [ "Dynamic Range Compression", null],
// "PSDYNVOL": [ "Dynamic Volume", null],
};
this.log = log;
this.ipaddress = ipaddress;
this.maxVol = 98.0; // good default
this.serverController = new telnet_client_1.default();
this.log.info("Connecting to Denon/Marantz Controller at ", ipaddress);
this.serverControllerConnect();
this.state = {};
this.reconnectOnErrRetires = 5;
for (const command of Object.keys(this.COMMANDS)) {
this.state[command] = '-';
}
}
async serverControllerConnect() {
let params = {
host: this.ipaddress,
port: serverControllerPort,
echoLines: 0,
irs: '\r',
negotiationMandatory: false,
ors: '\r',
separator: false,
shellPrompt: '',
timeout: 1500,
};
await this.serverController.connect(params);
this.serverController.on('data', this.serverControllerDataCallback.bind(this));
this.reconnectOnErrRetires = 5;
this.refresh();
}
async refresh() {
try {
Object.keys(this.COMMANDS).forEach((cmd) => {
// for status add question mark
this.serverControllerSend(`${cmd}?`);
});
this.log.info(`current state ${this.ipaddress} of controller ${this.state}`);
}
catch (error) {
this.log.error(`received error ${error}`);
if (this.reconnectOnErrRetires > 0) {
// try to connect again.
await this.serverControllerConnect();
this.reconnectOnErrRetires--;
}
else {
throw error;
}
}
}
serverControllerDataCallback(data) {
this.parseCommandResult(data.toString());
}
async serverControllerSend(data) {
// msleep(50);
let result = await this.serverController.exec(data);
this.parseCommandResult(result);
}
parseMany(cmd, data) {
this.log.info(`ParseMany ${cmd} with ${data}`);
this.state[cmd] = data;
}
// only works for main zone, others zones handled differently
parseMV(data) {
this.log.info(`ParseMv with ${data}`);
let isMax = false;
let value = "";
if (data.startsWith('MVMAX')) {
isMax = true;
value = data.split('MVMAX')[1].trim();
}
else {
value = data.split('MV')[1].trim();
}
let volume = Number(value);
if (value.length > 2) {
// multipled by 10 to not be float (example: 505 == 50.5)
volume = volume / 10;
}
this.log.info("new value", volume, value, value.length);
if (isMax) {
this.maxVol = volume;
}
else {
this.state['MV'] = volume;
}
}
parseZone(zone, data) {
this.log.info(`parseZone ${zone} with ${data}`);
if (['ON', 'OFF'].includes(data)) {
this.state[zone] = data;
return;
}
if (!isNaN(Number(data))) {
let value = Number(data);
if (data.length > 2) {
// multipled by 10 to not be float (example: 505 == 50.5)
value = value / 10;
}
this.state[`${zone}MV`] = value;
return;
}
// assume source inputs
if (types_1.INPUTS.includes(data)) {
this.state[`${zone}SI`] = data;
return;
}
}
parseZ2(data) {
this.parseZone('Z2', data.split('Z2')[1].trim());
}
parseZ3(data) {
this.parseZone('Z3', data.split('Z3')[1].trim());
}
parseCommandResult(data) {
this.log.info(`parsing result ${data}`);
let results = data.split('\r');
results.forEach((result) => {
Object.keys(this.COMMANDS).forEach((cmd) => {
if (result.startsWith(cmd)) {
if (this.COMMANDS[cmd][1] != null) {
this.COMMANDS[cmd][1](result);
}
else {
this.parseMany(cmd, result.split(cmd)[1].trim());
}
}
});
});
}
getPrefixByZone(zone) {
switch (zone) {
case 'main':
return ''; //'ZM'
case 'zone2':
return 'Z2';
case 'zone3':
return 'Z3';
case 'zone4':
return 'Z4';
default:
// cant happen
return '??';
}
}
parseBoolString(b) {
if (b == 'ON') {
return true;
}
else {
return false;
}
}
boolToString(b) {
if (b) {
return 'ON';
}
else {
return 'OFF';
}
}
GetPowerState(zone) {
let prefix = this.getPrefixByZone(zone);
if (zone === 'main') {
prefix = 'PW';
}
return this.parseBoolString(this.state[prefix]);
}
async SetPowerState(zone, value) {
let prefix = this.getPrefixByZone(zone);
let state = value ? 'ON' : 'OFF';
if (zone === 'main') {
prefix = 'PW';
state = value ? 'ON' : 'STANDBY';
}
await this.serverControllerSend(`${prefix}${state}`);
}
GetMuteState(zone) {
let commandPrefix = `${this.getPrefixByZone(zone)}MU`;
return this.parseBoolString(this.state[commandPrefix]);
}
async SetMuteSate(zone, value) {
let commandPrefix = `${this.getPrefixByZone(zone)}MU`;
await this.serverControllerSend(`${commandPrefix}${this.boolToString(value)}`);
}
GetVolume(zone) {
let commandPrefix = `${this.getPrefixByZone(zone)}MV`;
return Number(this.state[commandPrefix]);
}
async SetVolume(zone, value) {
// for some reason this does not work on anything but main zone,
if (value > this.maxVol) {
value = this.maxVol;
}
if ((Number(value) * 10) % 10) {
value = 5 * Math.round(Number(value) * 10 / 5);
}
this.log.info(`setting new volume: ${value}`);
// TODO: currently doesn't work with values below 10 because it needs to be padded by zero
let commandPrefix = `${this.getPrefixByZone(zone)}MV${value}`;
await this.serverControllerSend(commandPrefix);
}
GetInputSource(zone) {
let commandPrefix = `${this.getPrefixByZone(zone)}SI`;
return this.state[commandPrefix];
}
async SetInputSource(zone, source) {
if (zone == 'main') {
await this.serverControllerSend(`SI${source}`);
}
else {
await this.serverControllerSend(`${this.getPrefixByZone(zone)}${source}`);
}
}
}
exports.DenonMarantzController = DenonMarantzController;
//# sourceMappingURL=controller.js.map