UNPKG

homebridge-denon-tv

Version:

Homebridge plugin to control Denon/Marantz AV Receivers.

1,011 lines (908 loc) • 50.9 kB
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 ?