homebridge-denon-tv
Version:
Homebridge plugin to control Denon/Marantz AV Receivers.
1,011 lines (908 loc) • 50.9 kB
JavaScript
import { promises as fsPromises } from 'fs';
import EventEmitter from 'events';
import Mqtt from './mqtt.js';
import RestFul from './restful.js';
import Denon from './denon.js';
import { PictureModesConversionToHomeKit, PictureModesDenonNumber } from './constants.js';
let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
class Zone3 extends EventEmitter {
constructor(api, device, name, host, port, generation, zone, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile) {
super();
Accessory = api.platformAccessory;
Characteristic = api.hap.Characteristic;
Service = api.hap.Service;
Categories = api.hap.Categories;
Encode = api.hap.encode;
AccessoryUUID = api.hap.uuid;
//device configuration
this.name = name;
this.host = host;
this.port = port;
this.generation = generation;
this.zone = zone;
this.getInputsFromDevice = device.getInputsFromDevice || false;
this.getFavoritesFromDevice = device.getFavoritesFromDevice || false;
this.getQuickSmartSelectFromDevice = device.getQuickSmartSelectFromDevice || false;
this.inputsDisplayOrder = device.inputsDisplayOrder || 0;
this.inputs = device.inputs || [];
this.buttons = device.buttonsZ3 || [];
this.sensorPower = device.sensorPower || false;
this.sensorVolume = device.sensorVolume || false
this.sensorMute = device.sensorMute || false;
this.sensorInput = device.sensorInput || false;
this.sensorInputs = device.sensorInputs || [];
this.powerControlZone = device.powerControlZone || 0;
this.volumeControlNamePrefix = device.volumeControlNamePrefix || false;
this.volumeControlName = device.volumeControlName || 'Volume';
this.volumeControlType = device.volumeControlType || 0;
this.volumeControlZone = device.volumeControlZone || 0;
this.volumeMax = device.volumeMax || 100;
this.infoButtonCommand = device.infoButtonCommand || 'MNINF';
this.refreshInterval = device.refreshInterval * 1000 || 5000;
this.enableDebugMode = device.enableDebugMode || false;
this.disableLogInfo = device.disableLogInfo || false;
this.devInfoFile = devInfoFile;
this.inputsFile = inputsFile;
this.inputsNamesFile = inputsNamesFile;
this.inputsTargetVisibilityFile = inputsTargetVisibilityFile;
//external integration
this.restFul = device.restFul || {};
this.restFulConnected = false;
this.mqtt = device.mqtt || {};
this.mqttConnected = false;
//sensors
this.sensorsInputsConfigured = [];
for (const sensor of this.sensorInputs) {
const sensorInputName = sensor.name ?? false;
const sensorInputReference = sensor.reference ?? false;
const sensorInputDisplayType = sensor.displayType ?? 0;
if (sensorInputName && sensorInputReference && sensorInputDisplayType > 0) {
sensor.serviceType = ['', Service.MotionSensor, Service.OccupancySensor, Service.ContactSensor][sensorInputDisplayType];
sensor.characteristicType = ['', Characteristic.MotionDetected, Characteristic.OccupancyDetected, Characteristic.ContactSensorState][sensorInputDisplayType];
sensor.state = false;
this.sensorsInputsConfigured.push(sensor);
} else {
const log = sensorInputDisplayType === 0 ? false : this.emit('info', `Sensor Name: ${sensorInputName ? sensorInputName : 'Missing'}, Reference: ${sensorInputReference ? sensorInputReference : 'Missing'}`);
};
}
this.sensorsInputsConfiguredCount = this.sensorsInputsConfigured.length || 0;
//buttons
this.buttonsConfigured = [];
for (const button of this.buttons) {
const buttonName = button.name ?? false;
const buttonReference = button.reference ?? false;
const buttonDisplayType = button.displayType ?? 0;
if (buttonName && buttonReference && buttonDisplayType > 0) {
button.serviceType = ['', Service.Outlet, Service.Switch][buttonDisplayType];
button.state = false;
this.buttonsConfigured.push(button);
} else {
const log = buttonDisplayType === 0 ? false : this.emit('info', `Button Name: ${buttonName ? buttonName : 'Missing'}, Reference: ${buttonReference ? buttonReference : 'Missing'}`);
};
}
this.buttonsConfiguredCount = this.buttonsConfigured.length || 0;
//variable
this.startPrepareAccessory = true;
this.allServices = [];
this.inputsConfigured = [];
this.inputIdentifier = 1;
this.startPrepareAccessory = true;
this.power = false;
this.reference = '';
this.volume = 0;
this.volumeDisplay = false;
this.mute = true;
this.mediaState = false;
this.sensorVolumeState = false;
this.sensorInputState = false;
};
async saveData(path, data) {
try {
data = JSON.stringify(data, null, 2);
await fsPromises.writeFile(path, data);
const debug = this.enableDebugLog ? this.emit('debug', `Saved data: ${data}`) : false;
return true;
} catch (error) {
throw new Error(`Save data error: ${error}`);
};
}
async readData(path) {
try {
const data = await fsPromises.readFile(path);
return data;
} catch (error) {
throw new Error(`Read saved data error: ${error}`);
};
}
async sanitizeString(str) {
// Replace dots, colons, and semicolons inside words with a space
str = str.replace(/(\w)[.:;]+(\w)/g, '$1 $2');
// Remove remaining dots, colons, semicolons, plus, and minus anywhere in the string
str = str.replace(/[.:;+\-]/g, '');
// Replace all other invalid characters (anything not A-Z, a-z, 0-9, space, or apostrophe) with a space
str = str.replace(/[^A-Za-z0-9 ']/g, ' ');
// Trim leading and trailing spaces
str = str.trim();
return str;
}
async setOverExternalIntegration(integration, key, value) {
try {
let set = false
switch (key) {
case 'Power':
const powerState = value ? 'ON' : 'OFF';
set = await this.stateControl('Power', powerState);
break;
case 'Input':
const input = `Z3${value}`;
set = await this.denon.send(input);
break;
case 'Surround':
const surround = `MS${value}`;
set = await this.denon.send(surround);
break;
case 'Volume':
const volume = (value < 0 || value > 100) ? this.volume : (value < 10 ? `0${value}` : value);
set = await this.stateControl('Volume', volume);
break;
case 'Mute':
const mute = value ? 'ON' : 'OFF';
set = await this.stateControl('Mute', mute);
break;
case 'RcControl':
set = await this.denon.send(value);
break;
default:
this.emit('warn', `${integration}, received key: ${key}, value: ${value}`);
break;
};
return set;
} catch (error) {
throw new Error(`${integration} set key: ${key}, value: ${value}, error: ${error}`);
};
}
async externalIntegrations() {
try {
//RESTFul server
const restFulEnabled = this.restFul.enable || false;
if (restFulEnabled) {
this.restFul1 = new RestFul({
port: this.restFul.port || 3000,
debug: this.restFul.debug || false
});
this.restFul1.on('connected', (message) => {
this.emit('success', message);
this.restFulConnected = true;
})
.on('set', async (key, value) => {
try {
await this.setOverExternalIntegration('RESTFul', key, value);
} catch (error) {
this.emit('warn', `RESTFul set error: ${error}`);
};
})
.on('debug', (debug) => {
this.emit('debug', debug);
})
.on('warn', (warn) => {
this.emit('warn', warn);
})
.on('error', (error) => {
this.emit('error', error);
});
}
//mqtt client
const mqttEnabled = this.mqtt.enable || false;
if (mqttEnabled) {
this.mqtt1 = new Mqtt({
host: this.mqtt.host,
port: this.mqtt.port || 1883,
clientId: this.mqtt.clientId || `denon_${Math.random().toString(16).slice(3)}`,
prefix: `${this.mqtt.prefix}/${this.name}`,
user: this.mqtt.user,
passwd: this.mqtt.passwd,
debug: this.mqtt.debug || false
});
this.mqtt1.on('connected', (message) => {
this.emit('success', message);
this.mqttConnected = true;
})
.on('subscribed', (message) => {
this.emit('success', message);
})
.on('set', async (key, value) => {
try {
await this.setOverExternalIntegration('MQTT', key, value);
} catch (error) {
this.emit('warn', `MQTT set error: ${error}`);
};
})
.on('debug', (debug) => {
this.emit('debug', debug);
})
.on('warn', (warn) => {
this.emit('warn', warn);
})
.on('error', (error) => {
this.emit('error', error);
});
};
return true;
} catch (error) {
this.emit('warn', `External integration start error: ${error}`);
};
}
async prepareDataForAccessory() {
try {
//read inputs file
const savedInputs = await this.readData(this.inputsFile);
this.savedInputs = savedInputs.toString().trim() !== '' ? JSON.parse(savedInputs) : this.inputs;
const debug = this.enableDebugMode ? this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`) : false;
//read inputs names from file
const savedInputsNames = await this.readData(this.inputsNamesFile);
this.savedInputsNames = savedInputsNames.toString().trim() !== '' ? JSON.parse(savedInputsNames) : {};
const debug1 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`);
//read inputs visibility from file
const savedInputsTargetVisibility = await this.readData(this.inputsTargetVisibilityFile);
this.savedInputsTargetVisibility = savedInputsTargetVisibility.toString().trim() !== '' ? JSON.parse(savedInputsTargetVisibility) : {};
const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Read saved Inputs Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`);
return true;
} catch (error) {
throw new Error(`Prepare data for accessory error: ${error}`);
}
}
async displayOrder() {
try {
switch (this.inputsDisplayOrder) {
case 0:
this.inputsConfigured.sort((a, b) => a.identifier - b.identifier);
break;
case 1:
this.inputsConfigured.sort((a, b) => a.name.localeCompare(b.name));
break;
case 2:
this.inputsConfigured.sort((a, b) => b.name.localeCompare(a.name));
break;
case 3:
this.inputsConfigured.sort((a, b) => a.reference.localeCompare(b.reference));
break;
case 4:
this.inputsConfigured.sort((a, b) => b.reference.localeCompare(a.reference));
break;
}
const debug = !this.enableDebugMode ? false : this.emit('debug', `Inputs display order: ${JSON.stringify(this.inputsConfigured, null, 2)}`);
const displayOrder = this.inputsConfigured.map(input => input.identifier);
this.televisionService.setCharacteristic(Characteristic.DisplayOrder, Encode(1, displayOrder).toString('base64'));
return true;
} catch (error) {
throw new Error(`Display order error: ${error}`);
};
}
async startImpulseGenerator() {
try {
//start impulse generator
await this.denon.impulseGenerator.start([{ name: 'checkState', sampling: this.refreshInterval }]);
return true;
} catch (error) {
throw new Error(`Impulse generator start error: ${error}`);
};
}
async scaleValue(value, inMin, inMax, outMin, outMax) {
const scaledValue = parseFloat((((Math.max(inMin, Math.min(inMax, value)) - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin).toFixed(0));
return scaledValue;
}
async stateControl(type, value) {
try {
// Normalize value for Power type
if (type === 'Power' && value === 'OFF' && this.volumeControlZone === 7) {
value = 'STANDBY';
}
// Define main zone
const mainZone = type === 'Power' ? 'ZM' : 'MV';
const zoneMap = {
0: [mainZone],
1: ['Z2'],
2: ['Z3'],
3: ['Z2', 'Z3'],
4: [mainZone, 'Z2'],
5: [mainZone, 'Z3'],
6: [mainZone, 'Z2', 'Z3'],
7: ['PW']
};
// Reuse volume control zones for better readability
const typeMap = {
'Power': zoneMap[this.powerControlZone],
'VolumeSelector': zoneMap[this.volumeControlZone],
'Volume': zoneMap[this.volumeControlZone],
'Mute': zoneMap[this.volumeControlZone].map(zone => zone ? `${zone}MU` : 'MU')
};
// Get the commands for the specified type
const commands = typeMap[type];
if (commands) {
const commandsCount = commands.length;
for (let i = 0; i < commandsCount; i++) {
const cmd = commands[i];
await this.denon.send(`${cmd}${value}`);
const pauseTime = type === 'Power' && value === 'ON' && commandsCount > 1 && i === 0 ? 4000 : 75;
const pause = i < commandsCount - 1 ? await new Promise(resolve => setTimeout(resolve, pauseTime)) : false;
}
} else {
this.emit('warn', `Unknown control type: ${type}`);
}
return true;
} catch (error) {
this.emit('warn', `State control error for type ${type} with value ${value}: ${error}`);
}
}
//prepare accessory
async prepareAccessory() {
try {
//accessory
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare accessory`);
const accessoryName = this.name;
const accessoryUUID = AccessoryUUID.generate(this.serialNumber + this.zone);
const accessoryCategory = Categories.AUDIO_RECEIVER;
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
//information service
const debug1 = !this.enableDebugMode ? false : this.emit('debug', `Prepare information service`);
this.informationService = 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.allServices.push(this.informationService);
//prepare television service
const debug2 = !this.enableDebugMode ? false : this.emit('debug', `Prepare television service`);
this.televisionService = accessory.addService(Service.Television, `${accessoryName} Television`, 'Television');
this.televisionService.setCharacteristic(Characteristic.ConfiguredName, accessoryName);
this.televisionService.setCharacteristic(Characteristic.SleepDiscoveryMode, 1);
this.televisionService.getCharacteristic(Characteristic.Active)
.onGet(async () => {
const state = this.power;
return state;
})
.onSet(async (state) => {
if (this.power == state) {
return;
}
try {
const powerState = state ? 'ON' : 'OFF';
await this.stateControl('Power', powerState);
const info = this.disableLogInfo ? false : this.emit('info', `set Power: ${powerState}`);
} catch (error) {
this.emit('warn', `set Power error: ${error}`);
};
});
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
.onGet(async () => {
const inputIdentifier = this.inputIdentifier;
return inputIdentifier;
})
.onSet(async (activeIdentifier) => {
try {
const input = this.inputsConfigured.find(input => input.identifier === activeIdentifier);
const inputName = input.name;
const inputMode = input.mode;
const inputReference = input.reference;
const reference = `${inputMode}${inputReference}`;
switch (this.power) {
case false:
await new Promise(resolve => setTimeout(resolve, 4000));
const tryAgain = this.power ? this.televisionService.setCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier) : false;
break;
case true:
await this.denon.send(reference);
const info = this.disableLogInfo ? false : this.emit('info', `set Input Name: ${inputName}, Reference: ${reference}`);
break;
}
} catch (error) {
this.emit('warn', `set Input error: ${error}`);
};
});
this.televisionService.getCharacteristic(Characteristic.RemoteKey)
.onSet(async (command) => {
try {
const rcMedia = this.inputReference === 'SPOTIFY' || this.inputReference === 'BT' || this.inputReference === 'USB/IPOD' || this.inputReference === 'NET' || this.inputReference === 'MPLAY';
switch (command) {
case Characteristic.RemoteKey.REWIND:
command = rcMedia ? 'NS9E' : 'MN9E';
break;
case Characteristic.RemoteKey.FAST_FORWARD:
command = rcMedia ? 'NS9D' : 'MN9D';
break;
case Characteristic.RemoteKey.NEXT_TRACK:
command = rcMedia ? 'MN9D' : 'MN9F';
break;
case Characteristic.RemoteKey.PREVIOUS_TRACK:
command = rcMedia ? 'MN9E' : 'MN9G';
break;
case Characteristic.RemoteKey.ARROW_UP:
command = rcMedia ? 'NS90' : 'MNCUP';
break;
case Characteristic.RemoteKey.ARROW_DOWN:
command = rcMedia ? 'NS91' : 'MNCDN';
break;
case Characteristic.RemoteKey.ARROW_LEFT:
command = rcMedia ? 'NS92' : 'MNCLT';
break;
case Characteristic.RemoteKey.ARROW_RIGHT:
command = rcMedia ? 'NS93' : 'MNENT';
break;
case Characteristic.RemoteKey.SELECT:
command = rcMedia ? 'NS94' : 'MNENT';
break;
case Characteristic.RemoteKey.BACK:
command = rcMedia ? 'MNRTN' : 'MNRTN';
break;
case Characteristic.RemoteKey.EXIT:
command = rcMedia ? 'MNRTN' : 'MNRTN';
break;
case Characteristic.RemoteKey.PLAY_PAUSE:
command = rcMedia ? (this.mediaState ? 'NS9B' : 'NS9A') : 'NS94';
this.mediaState = !this.mediaState;
break;
case Characteristic.RemoteKey.INFORMATION:
command = this.infoButtonCommand;
break;
}
await this.denon.send(command);
const info = this.disableLogInfo ? false : this.emit('info', `set Remote Key: ${command}`);
} catch (error) {
this.emit('warn', `set Remote Key error: ${error}`);
};
});
this.allServices.push(this.televisionService);
//prepare speaker service
const debug3 = !this.enableDebugMode ? false : this.emit('debug', `Prepare speaker service`);
this.speakerService = accessory.addService(Service.TelevisionSpeaker, `${accessoryName} Speaker`, 'Speaker');
this.speakerService.getCharacteristic(Characteristic.Active)
.onGet(async () => {
const state = this.power;
return state;
})
.onSet(async (state) => {
});
this.speakerService.getCharacteristic(Characteristic.VolumeControlType)
.onGet(async () => {//none, relative, relative with current, absolute
const state = 3;
return state;
})
this.speakerService.getCharacteristic(Characteristic.VolumeSelector)
.onSet(async (command) => {
try {
switch (command) {
case Characteristic.VolumeSelector.INCREMENT:
command = 'UP';
await this.stateControl('VolumeSelector', command);
break;
case Characteristic.VolumeSelector.DECREMENT:
command = 'DOWN';
await this.stateControl('VolumeSelector', command);
break;
}
const info = this.disableLogInfo ? false : this.emit('info', `set Volume Selector: ${command}`);
} catch (error) {
this.emit('warn', `set Volume Selector error: ${error}`);
};
});
this.speakerService.getCharacteristic(Characteristic.Volume)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (value) => {
try {
value = value > this.volumeMax ? this.volumeMax : value;
let scaledValue = await this.scaleValue(value, 0, 100, 0, 98);
scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue;
await this.stateControl('Volume', scaledValue);
const info = this.disableLogInfo ? false : this.emit('info', `set Volume: ${value}%`);
} catch (error) {
this.emit('warn', `set Volume error: ${error}`);
};
});
this.speakerService.getCharacteristic(Characteristic.Mute)
.onGet(async () => {
const state = this.mute;
return state;
})
.onSet(async (state) => {
try {
state = state ? 'ON' : 'OFF';
await this.stateControl('Mute', state);
const info = this.disableLogInfo ? false : this.emit('info', `set Mute: ${state}`);
} catch (error) {
this.emit('warn', `set Mute error: ${error}`);
};
});
this.allServices.push(this.speakerService);
//prepare inputs service
const debug8 = !this.enableDebugMode ? false : this.emit('debug', `Prepare inputs services`);
//check possible inputs count (max 85)
const inputs = this.savedInputs;
const inputsCount = inputs.length;
const possibleInputsCount = 85 - this.allServices.length;
const maxInputsCount = inputsCount >= possibleInputsCount ? possibleInputsCount : inputsCount;
for (let i = 0; i < maxInputsCount; i++) {
//input
const input = inputs[i];
const inputIdentifier = i + 1;
//get input reference
const inputReference = input.reference;
//get input name
const name = input.name ?? `Input ${inputIdentifier}`;
//get input name
const savedInputsName = this.savedInputsNames[inputReference] ?? false;
input.name = savedInputsName ? savedInputsName.substring(0, 64) : name.substring(0, 64);
//get type
const inputSourceType = 0;
//get configured
const isConfigured = 1;
//get visibility
input.visibility = this.savedInputsTargetVisibility[inputReference] ?? 0;
//add identifier to the input
input.identifier = inputIdentifier;
//input service
const sanitizedName = await this.sanitizeString(input.name);
const inputService = accessory.addService(Service.InputSource, sanitizedName, `Input ${inputIdentifier}`);
inputService
.setCharacteristic(Characteristic.Identifier, inputIdentifier)
.setCharacteristic(Characteristic.Name, sanitizedName)
.setCharacteristic(Characteristic.IsConfigured, isConfigured)
.setCharacteristic(Characteristic.InputSourceType, inputSourceType)
.setCharacteristic(Characteristic.CurrentVisibilityState, input.visibility)
inputService.getCharacteristic(Characteristic.ConfiguredName)
.onGet(async () => {
return sanitizedName;
})
.onSet(async (value) => {
try {
input.name = value;
this.savedInputsNames[inputReference] = value;
await this.saveData(this.inputsNamesFile, this.savedInputsNames);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Saved Input Name: ${value}, Reference: ${inputReference}`);
//sort inputs
const index = this.inputsConfigured.findIndex(input => input.reference === inputReference);
this.inputsConfigured[index].name = value;
await this.displayOrder();
} catch (error) {
this.emit('warn', `Save Input Name error: ${error}`);
}
});
inputService.getCharacteristic(Characteristic.TargetVisibilityState)
.onGet(async () => {
return input.visibility;
})
.onSet(async (state) => {
try {
input.visibility = state;
this.savedInputsTargetVisibility[inputReference] = state;
await this.saveData(this.inputsTargetVisibilityFile, this.savedInputsTargetVisibility);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Saved Input: ${input.name} Target Visibility: ${state ? 'HIDEN' : 'SHOWN'}`);
} catch (error) {
this.emit('warn', `Save Input Target Visibility error: ${error}`);
}
});
this.inputsConfigured.push(input);
this.televisionService.addLinkedService(inputService);
this.allServices.push(inputService);
};
//prepare volume service
if (this.volumeControlType > 0) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare volume service`);
const volumeServiceName = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName;
if (this.volumeControlType === 1) {
this.volumeService = accessory.addService(Service.Lightbulb, `${volumeServiceName}`, `${volumeServiceName}`);
this.volumeService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.volumeService.setCharacteristic(Characteristic.ConfiguredName, `${volumeServiceName}`);
this.volumeService.getCharacteristic(Characteristic.Brightness)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (value) => {
this.speakerService.setCharacteristic(Characteristic.Volume, value);
});
this.volumeService.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = !this.mute;
return state;
})
.onSet(async (state) => {
this.speakerService.setCharacteristic(Characteristic.Mute, !state);
});
this.allServices.push(this.volumeService);
}
if (this.volumeControlType === 2) {
this.volumeServiceFan = accessory.addService(Service.Fan, `${volumeServiceName}`, `${volumeServiceName}`);
this.volumeServiceFan.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.volumeServiceFan.setCharacteristic(Characteristic.ConfiguredName, `${volumeServiceName}`);
this.volumeServiceFan.getCharacteristic(Characteristic.RotationSpeed)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (value) => {
this.speakerService.setCharacteristic(Characteristic.Volume, value);
});
this.volumeServiceFan.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = !this.mute;
return state;
})
.onSet(async (state) => {
this.speakerService.setCharacteristic(Characteristic.Mute, !state);
});
this.allServices.push(this.volumeServiceFan);
}
};
//prepare sensor service
if (this.sensorPower) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare power sensor service`);
this.sensorPowerService = accessory.addService(Service.ContactSensor, `${this.sZoneName} Power Sensor`, `Power Sensor`);
this.sensorPowerService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorPowerService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Power Sensor`);
this.sensorPowerService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.power;
return state;
});
this.allServices.push(this.sensorPowerService);
};
if (this.sensorVolume) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare volume sensor service`);
this.sensorVolumeService = accessory.addService(Service.ContactSensor, `${this.sZoneName} Volume Sensor`, `Volume Sensor`);
this.sensorVolumeService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorVolumeService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Volume Sensor`);
this.sensorVolumeService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.sensorVolumeState;
return state;
});
this.allServices.push(this.sensorVolumeService);
};
if (this.sensorMute) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare mute sensor service`);
this.sensorMuteService = accessory.addService(Service.ContactSensor, `${this.sZoneName} Mute Sensor`, `Mute Sensor`);
this.sensorMuteService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorMuteService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Mute Sensor`);
this.sensorMuteService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.power ? this.mute : false;
return state;
});
this.allServices.push(this.sensorMuteService);
};
if (this.sensorInput) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare input sensor service`);
this.sensorInputService = accessory.addService(Service.ContactSensor, `${this.sZoneName} Input Sensor`, `Input Sensor`);
this.sensorInputService.addOptionalCharacteristic(Characteristic.ConfiguredName);
this.sensorInputService.setCharacteristic(Characteristic.ConfiguredName, `${accessoryName} Input Sensor`);
this.sensorInputService.getCharacteristic(Characteristic.ContactSensorState)
.onGet(async () => {
const state = this.sensorInputState;
return state;
});
this.allServices.push(this.sensorInputService);
};
//prepare sonsor service
const possibleSensorInputsCount = 99 - this.allServices.length;
const maxSensorInputsCount = this.sensorsInputsConfiguredCount >= possibleSensorInputsCount ? possibleSensorInputsCount : this.sensorsInputsConfiguredCount;
if (maxSensorInputsCount > 0) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare inputs sensors services`);
this.sensorsInputsServices = [];
for (let i = 0; i < maxSensorInputsCount; i++) {
//get sensor
const sensorInput = this.sensorsInputsConfigured[i];
//get sensor name
const sensorInputName = sensorInput.name;
//get sensor name prefix
const namePrefix = sensorInput.namePrefix || false;
//get service type
const serviceType = sensorInput.serviceType;
//get service type
const characteristicType = sensorInput.characteristicType;
const serviceName = namePrefix ? `${accessoryName} ${sensorInputName}` : sensorInputName;
const sensorInputService = new serviceType(serviceName, `Sensor ${i}`);
sensorInputService.addOptionalCharacteristic(Characteristic.ConfiguredName);
sensorInputService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
sensorInputService.getCharacteristic(characteristicType)
.onGet(async () => {
const state = sensorInput.state
return state;
});
this.sensorsInputsServices.push(sensorInputService);
this.allServices.push(sensorInputService);
accessory.addService(sensorInputService);
}
};
//prepare buttons services
const possibleButtonsCount = 99 - this.allServices.length;
const maxButtonsCount = this.buttonsConfiguredCount >= possibleButtonsCount ? possibleButtonsCount : this.buttonsConfiguredCount;
if (maxButtonsCount > 0) {
const debug = !this.enableDebugMode ? false : this.emit('debug', `Prepare buttons services`);
this.buttonsServices = [];
for (let i = 0; i < maxButtonsCount; i++) {
//get button
const button = this.buttonsConfigured[i];
//get button name
const buttonName = button.name;
//get button reference
const buttonReference = button.reference;
//get button name prefix
const namePrefix = button.namePrefix || false;
//get service type
const serviceType = button.serviceType;
const serviceName = namePrefix ? `${accessoryName} ${buttonName}` : buttonName;
const buttonService = new serviceType(serviceName, `Button ${i}`);
buttonService.addOptionalCharacteristic(Characteristic.ConfiguredName);
buttonService.setCharacteristic(Characteristic.ConfiguredName, serviceName);
buttonService.getCharacteristic(Characteristic.On)
.onGet(async () => {
const state = button.state;
return state;
})
.onSet(async (state) => {
try {
const reference = `Z3${buttonReference.substring(1)}`;
const set = state ? await this.denon.send(reference) : false;
const info = this.disableLogInfo || !state ? false : this.emit('info', `set Button Name: ${buttonName}, Reference: ${reference}`);
} catch (error) {
this.emit('warn', `set Button error: ${error}`);
};
});
this.buttonsServices.push(buttonService);
this.allServices.push(buttonService);
accessory.addService(buttonService);
};
};
//sort inputs list
await this.displayOrder();
return accessory;
} catch (error) {
throw new Error(error)
};
}
//start
async start() {
try {
//denon client
this.denon = new Denon({
host: this.host,
port: this.port,
generation: this.generation,
zone: this.zone,
inputs: this.inputs,
devInfoFile: this.devInfoFile,
inputsFile: this.inputsFile,
getInputsFromDevice: this.getInputsFromDevice,
getFavoritesFromDevice: this.getFavoritesFromDevice,
getQuickSmartSelectFromDevice: this.getQuickSmartSelectFromDevice,
enableDebugLog: this.enableDebugMode
});
this.denon.on('deviceInfo', (manufacturer, modelName, serialNumber, firmwareRevision, deviceZones, apiVersion, supportPictureMode) => {
this.emit('devInfo', `-------- ${this.name} --------`);
this.emit('devInfo', `Manufacturer: ${manufacturer}`);
this.emit('devInfo', `Model: ${modelName}`);
this.emit('devInfo', `Control: Zone 3`);
this.emit('devInfo', `----------------------------------`);
this.manufacturer = manufacturer;
this.modelName = modelName;
this.serialNumber = serialNumber;
this.firmwareRevision = firmwareRevision;
})
.on('stateChanged', async (power, reference, volume, volumeDisplay, mute, pictureMode) => {
const input = this.inputsConfigured.find(input => input.reference === reference) ?? false;
const inputIdentifier = input ? input.identifier : this.inputIdentifier;
const scaledVolume = await this.scaleValue(volume, -80, 18, 0, 100);
mute = power ? mute : true;
const pictureModeHomeKit = PictureModesConversionToHomeKit[pictureMode] ?? this.pictureMode;
if (this.televisionService) {
this.televisionService
.updateCharacteristic(Characteristic.Active, power)
.updateCharacteristic(Characteristic.PictureMode, pictureModeHomeKit);
}
if (this.televisionService) {
this.televisionService
.updateCharacteristic(Characteristic.ActiveIdentifier, inputIdentifier)
}
if (this.speakerService) {
this.speakerService
.updateCharacteristic(Characteristic.Active, power)
.updateCharacteristic(Characteristic.Volume, scaledVolume)
.updateCharacteristic(Characteristic.Mute, mute);
if (this.volumeService) {
this.volumeService
.updateCharacteristic(Characteristic.Brightness, scaledVolume)
.updateCharacteristic(Characteristic.On, !mute);
}
if (this.volumeServiceFan) {
this.volumeServiceFan
.updateCharacteristic(Characteristic.RotationSpeed, scaledVolume)
.updateCharacteristic(Characteristic.On, !mute);
}
}
//sensors
if (this.sensorPowerService) {
this.sensorPowerService
.updateCharacteristic(Characteristic.ContactSensorState, power)
}
if (this.sensorVolumeService && scaledVolume !== this.volume) {
for (let i = 0; i < 2; i++) {
const state = power ? [true, false][i] : false;
this.sensorVolumeService
.updateCharacteristic(Characteristic.ContactSensorState, state)
this.sensorVolumeState = state;
}
}
if (this.sensorMuteService) {
const state = power ? mute : false;
this.sensorMuteService
.updateCharacteristic(Characteristic.ContactSensorState, state)
}
if (this.sensorInputService && reference !== this.reference) {
for (let i = 0; i < 2; i++) {
const state = power ? [true, false][i] : false;
this.sensorInputService
.updateCharacteristic(Characteristic.ContactSensorState, state)
this.sensorInputState = state;
}
}
if (this.sensorsInputsServices) {
for (let i = 0; i < this.sensorsInputsConfiguredCount; i++) {
const sensorInput = this.sensorsInputsConfigured[i];
const state = power ? sensorInput.reference === reference : false;
sensorInput.state = state;
const characteristicType = sensorInput.characteristicType;
this.sensorsInputsServices[i]
.updateCharacteristic(characteristicType, state);
}
}
//buttons
if (this.buttonsServices) {
for (let i = 0; i < this.buttonsConfiguredCount; i++) {
const button = this.buttonsConfigured[i];
const state = this.power ? button.reference === reference : false;
button.state = state;
this.buttonsServices[i]
.updateCharacteristic(Characteristic.On, state);
}
}
this.inputIdentifier = inputIdentifier;
this.power = power;
this.reference = reference;
this.volume = scaledVolume;
this.mute = mute;
this.volumeDisplay = volumeDisplay;
this.pictureMode = pictureModeHomeKit;
if (!this.disableLogInfo) {
const name = input ? input.name : reference;
this.emit('info', `Power: ${power ? 'ON' : 'OFF'}`);
this.emit('info', `Input Name: ${name}`);
this.emit('info', `Reference: ${reference}`);
this.emit('info', `Mute: ${mute ? 'ON' : 'OFF'}`);
this.emit('info', `Volume: ${volumeDisplay !== 'Absolute' ? volume : scaledVolume}${volumeDisplay !== 'Absolute' ? 'dB' : '%'}`);
const emitInfo1 = volumeDisplay === false ? false : this.emit('info', `Volume Display: ${volumeDisplay}`);
this.emit('info', `Picture Mode: ${PictureModesDenonNumber[pictureMode]}`);
};
})
.on('success', (success) => {
this.emit('success', success);
})
.on('info', (info) => {
this.emit('info', info);
})
.on('debug', (debug) => {
this.emit('debug', debug);
})
.on('warn', (warn) => {
this.emit('warn', warn);
})
.on('error', (error) => {
this.emit('error', error);
})
.on('restFul', (path, data) => {
const restFul = this.restFulConnected ? this.restFul1.update(path, data) : false;
})
.on('mqtt', (topic, message) => {
const mqtt = this.mqttConnected ?