UNPKG

homebridge-denon-tv

Version:

Homebridge plugin to control Denon/Marantz AV Receivers.

904 lines (830 loc) 56.7 kB
import EventEmitter from 'events'; import Zone from './zone.js'; import Functions from './functions.js'; import { PictureModesConversionToHomeKit, PictureModesDenonNumber } from './constants.js'; let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID; class Zone3 extends EventEmitter { constructor(api, denon, denonInfo, device, devInfoFile, inputsFile, inputsNamesFile, inputsTargetVisibilityFile, restFul1 = null, restFulConnected = false, mqtt1 = null, mqttConnected = false) { 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.denon = denon; this.denonInfo = denonInfo; this.device = device; this.name = device.name; this.zoneControl = device.zoneControl; this.inputsDisplayOrder = device.inputs?.displayOrder || 0; this.buttons = (device.buttonsZ3 ?? []).filter(button => (button.displayType ?? 0) > 0); this.sensors = Array.isArray(device.sensors) ? (device.sensors ?? []).filter(sensor => (sensor.displayType ?? 0) > 0 && (sensor.mode ?? -1) >= 0) : []; this.powerControlZone = device.power?.zone || 0; this.volumeControl = device.volume?.displayType || 0; this.volumeControlZone = device.volume?.zone || 0; this.volumeControlName = device.volume?.name || 'Volume'; this.volumeControlNamePrefix = device.volume?.namePrefix || false; this.volumeControlMax = device.volume?.max || 100; this.logInfo = device.log?.info || false; this.logWarn = device.log?.warn || true; this.logDebug = device.log?.debug || false; this.infoButtonCommand = device.infoButtonCommand || 'MNINF'; this.devInfoFile = devInfoFile; this.inputsFile = inputsFile; this.inputsNamesFile = inputsNamesFile; this.inputsTargetVisibilityFile = inputsTargetVisibilityFile; //external integration this.restFul = device.restFul || {}; this.restFul1 = restFul1; this.restFulConnected = restFulConnected; this.mqtt = device.mqtt || {}; this.mqtt1 = mqtt1; this.mqttConnected = mqttConnected; //sensors for (const sensor of this.sensors) { sensor.serviceType = [null, Service.MotionSensor, Service.OccupancySensor, Service.ContactSensor][sensor.displayType]; sensor.characteristicType = [null, Characteristic.MotionDetected, Characteristic.OccupancyDetected, Characteristic.ContactSensorState][sensor.displayType]; sensor.state = false; } //buttons for (const button of this.buttons) { button.serviceType = [null, Service.Outlet, Service.Switch][button.displayType]; button.state = false; } //variable this.functions = new Functions(); this.inputIdentifier = 1; this.power = false; this.reference = ''; this.volume = 0; this.volumeDisplay = false; this.mute = false; this.playState = false; this.sensorVolumeState = false; this.sensorInputState = false; } async stateControl(type, value) { try { // Normalize value for Power type value = this.powerControlZone === 7 && type === 'Power' && value === 'OFF' ? 'STANDBY' : value; // Define main zone const mainZone = type === 'Power' ? 'ZM' : (type === 'Volume' || type === 'VolumeSelector') ? 'MV' : 'MU'; 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 zone for better readability const typeMap = { 'Power': zoneMap[this.powerControlZone], 'VolumeSelector': zoneMap[this.volumeControlZone], 'Volume': zoneMap[this.volumeControlZone], 'Mute': zoneMap[this.volumeControlZone] }; // 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 = type === 'Mute' && commands[i] !== 'MU' ? `${commands[i]}MU` : commands[i]; await this.denon.send(`${cmd}${value}`); const pauseTime = type === 'Power' && value === 'ON' && commandsCount > 1 && i === 0 ? 4000 : 75; if (i < commandsCount - 1) await new Promise(resolve => setTimeout(resolve, pauseTime)); } } else { if (this.logWarn) this.emit('warn', `Unknown control type: ${type}`); } return true; } catch (error) { if (this.logWarn) this.emit('warn', `State control error for type ${type} with value ${value}: ${error}`); } } 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 = `Z2${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 prepareDataForAccessory() { try { //read dev info from file this.savedInfo = await this.functions.readData(this.devInfoFile, true) ?? {}; if (this.logDebug) this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`); //read inputs file this.savedInputs = await this.functions.readData(this.inputsFile, true) ?? []; if (this.logDebug) this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`); //read inputs names from file this.savedInputsNames = await this.functions.readData(this.inputsNamesFile, true) ?? {}; if (this.logDebug) this.emit('debug', `Read saved Inputs Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`); //read inputs visibility from file this.savedInputsTargetVisibility = await this.functions.readData(this.inputsTargetVisibilityFile, true) ?? {}; if (this.logDebug) 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 { const sortStrategies = { 1: (a, b) => a.name.localeCompare(b.name), 2: (a, b) => b.name.localeCompare(a.name), 3: (a, b) => a.reference.localeCompare(b.reference), 4: (a, b) => b.reference.localeCompare(a.reference), }; const sortFn = sortStrategies[this.inputsDisplayOrder]; // Sort only if a valid function exists if (sortFn) { this.inputsServices.sort(sortFn); } // Debug if (this.logDebug) { const orderDump = this.inputsServices.map(svc => ({ name: svc.name, reference: svc.reference, identifier: svc.identifier, })); this.emit('debug', `Inputs display order:\n${JSON.stringify(orderDump, null, 2)}`); } // Always update DisplayOrder characteristic, even for "none" const displayOrder = this.inputsServices.map(svc => svc.identifier); const encodedOrder = Encode(1, displayOrder).toString('base64'); this.televisionService.updateCharacteristic(Characteristic.DisplayOrder, encodedOrder); return; } catch (error) { throw new Error(`Display order error: ${error}`); } } async addRemoveOrUpdateInput(inputs, remove = false) { try { if (!this.inputsServices) return; let updated = false; for (const input of inputs) { if (this.inputsServices.length >= 85 && !remove) continue; const inputReference = input.reference; const savedName = this.savedInputsNames[inputReference] ?? input.name; const sanitizedName = await this.functions.sanitizeString(savedName); const inputMode = input.mode ?? 0; const inputZonePrefix = input.zonePrefix; const inputVisibility = this.savedInputsTargetVisibility[inputReference] ?? 0; if (remove) { const svc = this.inputsServices.find(s => s.reference === inputReference); if (svc) { if (this.logDebug) this.emit('debug', `Removing input: ${input.name}, reference: ${inputReference}`); this.accessory.removeService(svc); this.inputsServices = this.inputsServices.filter(s => s.reference !== inputReference); updated = true; } continue; } let inputService = this.inputsServices.find(s => s.reference === inputReference); if (inputService) { const nameChanged = inputService.name !== sanitizedName; if (nameChanged) { inputService.name = sanitizedName; inputService .updateCharacteristic(Characteristic.Name, sanitizedName) .updateCharacteristic(Characteristic.ConfiguredName, sanitizedName); if (this.logDebug) this.emit('debug', `Updated Input: ${input.name}, reference: ${inputReference}`); updated = true; } } else { const identifier = this.inputsServices.length + 1; inputService = this.accessory.addService(Service.InputSource, sanitizedName, `Input ${inputReference}`); inputService.identifier = identifier; inputService.reference = inputReference; inputService.name = sanitizedName; inputService.mode = inputMode; inputService.zonePrefix = inputZonePrefix; inputService.visibility = inputVisibility; inputService .setCharacteristic(Characteristic.Identifier, identifier) .setCharacteristic(Characteristic.Name, sanitizedName) .setCharacteristic(Characteristic.ConfiguredName, sanitizedName) .setCharacteristic(Characteristic.IsConfigured, 1) .setCharacteristic(Characteristic.InputSourceType, inputMode) .setCharacteristic(Characteristic.CurrentVisibilityState, inputVisibility) .setCharacteristic(Characteristic.TargetVisibilityState, inputVisibility); // ConfiguredName persistence inputService.getCharacteristic(Characteristic.ConfiguredName) .onSet(async (value) => { try { value = await this.functions.sanitizeString(value); inputService.name = value; this.savedInputsNames[inputReference] = value; await this.functions.saveData(this.inputsNamesFile, this.savedInputsNames); if (this.logDebug) this.emit('debug', `Saved Input: ${input.name}, reference: ${inputReference}`); await this.displayOrder(); } catch (error) { if (this.logWarn) this.emit('warn', `Save Input Name error: ${error}`); } }); // TargetVisibility persistence inputService.getCharacteristic(Characteristic.TargetVisibilityState) .onSet(async (state) => { try { inputService.visibility = state; this.savedInputsTargetVisibility[inputReference] = state; await this.functions.saveData(this.inputsTargetVisibilityFile, this.savedInputsTargetVisibility); if (this.logDebug) this.emit('debug', `Saved Input: ${input.name}, reference: ${inputReference}, target visibility: ${state ? 'HIDDEN' : 'SHOWN'}`); } catch (error) { if (this.logWarn) this.emit('warn', `Save Target Visibility error: ${error}`); } }); this.inputsServices.push(inputService); this.televisionService.addLinkedService(inputService); if (this.logDebug) this.emit('debug', `Added Input: ${input.name}, reference: ${inputReference}`); updated = true; } } // Only one time run if (updated) await this.displayOrder(); return true; } catch (error) { throw new Error(`Add/Remove/Update input error: ${error}`); } } //prepare accessory async prepareAccessory() { try { //accessory if (this.logDebug) this.emit('debug', `Prepare accessory`); const accessoryName = this.name; const accessoryUUID = AccessoryUUID.generate(this.savedInfo.serialNumber + this.zoneControl); const accessoryCategory = Categories.AUDIO_RECEIVER; const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory); this.accessory = accessory; //information service if (this.logDebug) this.emit('debug', `Prepare information service`); this.informationService = accessory.getService(Service.AccessoryInformation) .setCharacteristic(Characteristic.Manufacturer, this.savedInfo.manufacturer) .setCharacteristic(Characteristic.Model, this.savedInfo.modelName) .setCharacteristic(Characteristic.SerialNumber, this.savedInfo.serialNumber) .setCharacteristic(Characteristic.FirmwareRevision, this.savedInfo.firmwareRevision); //prepare television service if (this.logDebug) 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 (!!state === this.power) return; try { const powerState = state ? 'ON' : 'OFF'; await this.stateControl('Power', powerState); if (this.logInfo) this.emit('info', `set Power: ${powerState}`); } catch (error) { if (this.logWarn) 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.inputsServices.find(i => i.identifier === activeIdentifier); if (!input) { if (this.logWarn) this.emit('warn', `Input with identifier ${activeIdentifier} not found`); return; } const { zonePrefix, name, reference } = input; if (!this.power) { if (this.logDebug) this.emit('debug', `AVR is off, deferring input switch to '${activeIdentifier}'`); (async () => { for (let attempt = 0; attempt < 3; attempt++) { await new Promise(resolve => setTimeout(resolve, 4000)); // if AVR on if (this.power) { // if input didn't switch → retry command if (this.inputIdentifier !== activeIdentifier) { if (this.logDebug) this.emit('debug', `Retrying input switch (${attempt + 1}/3)`); await this.denon.send(`${zonePrefix}${reference}`); } else { // success this.televisionService.updateCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier); if (this.logInfo) this.emit('info', `Input set successfully: ${name}`); return; } } } if (this.logWarn) this.emit('warn', `Failed to set input after retries: ${name}`); })().catch(err => { if (this.logWarn) this.emit('warn', `retry error: ${err}`); }); return; } // AVR is on await this.denon.send(`${zonePrefix}${reference}`); if (this.logInfo) this.emit('info', `set Input Name: ${name}, Reference: ${reference}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Input error: ${error}`); } }); this.televisionService.getCharacteristic(Characteristic.RemoteKey) .onSet(async (command) => { try { const rcMedia = this.reference === 'SPOTIFY' || this.reference === 'BT' || this.reference === 'USB/IPOD' || this.reference === 'NET' || this.reference === '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.playState ? 'NS9B' : 'NS9A') : 'NS94'; this.playState = !this.playState; break; case Characteristic.RemoteKey.INFORMATION: command = this.infoButtonCommand; break; } await this.denon.send(command); if (this.logInfo) this.emit('info', `set Remote Key: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Remote Key error: ${error}`); } }); //prepare inputs service if (this.logDebug) this.emit('debug', `Prepare inputs services`); this.inputsServices = []; await this.addRemoveOrUpdateInput(this.savedInputs, false); //Prepare volume service if (this.volumeControl > 0) { const volumeServiceName = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName; switch (this.volumeControl) { case 1: //lightbulb if (this.logDebug) this.emit('debug', `Prepare volume service lightbulb`); this.volumeServiceLightbulb = accessory.addService(Service.Lightbulb, volumeServiceName, 'Lightbulb Speaker'); this.volumeServiceLightbulb.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceLightbulb.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceLightbulb.getCharacteristic(Characteristic.Brightness) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { value = value > this.volumeControlMax ? this.volumeControlMax : value; let scaledValue = Math.min(Math.round(value), 98); scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue; await this.stateControl('Volume', scaledValue); if (this.logInfo) this.emit('info', `set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume error: ${error}`); } }); this.volumeServiceLightbulb.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { try { state = !state ? 'ON' : 'OFF'; await this.stateControl('Mute', state); if (this.logInfo) this.emit('info', `set Mute: ${state}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Mute error: ${error}`); } }); break; case 2: //fan if (this.logDebug) this.emit('debug', `Prepare volume service fan`); this.volumeServiceFan = accessory.addService(Service.Fan, volumeServiceName, 'Fan Speaker'); 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) => { try { value = value > this.volumeControlMax ? this.volumeControlMax : value; let scaledValue = Math.min(Math.round(value), 98); scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue; await this.stateControl('Volume', scaledValue); if (this.logInfo) this.emit('info', `set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume error: ${error}`); } }); this.volumeServiceFan.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { try { state = !state ? 'ON' : 'OFF'; await this.stateControl('Mute', state); if (this.logInfo) this.emit('info', `set Mute: ${state}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Mute error: ${error}`); } }); break; case 3: // tv speaker if (this.logDebug) this.emit('debug', `Prepare television speaker service`); const volumeServiceName3 = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName; this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName3, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName3); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.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; } if (this.logInfo) this.emit('info', `set Volume Selector: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume Selector error: ${error}`); }; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { value = value > this.volumeControlMax ? this.volumeControlMax : value; let scaledValue = Math.min(Math.round(value), 98); scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue; await this.stateControl('Volume', scaledValue); if (this.logInfo) this.emit('info', `set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { state = state ? 'ON' : 'OFF'; await this.stateControl('Mute', state); if (this.logInfo) this.emit('info', `set Mute: ${state}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Mute error: ${error}`); } }); break; case 4: // tv speaker + lightbulb if (this.logDebug) this.emit('debug', `Prepare television speaker service`); this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.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; } if (this.logInfo) this.emit('info', `set Volume Selector: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume Selector error: ${error}`); }; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { value = value > this.volumeControlMax ? this.volumeControlMax : value; let scaledValue = Math.min(Math.round(value), 98); scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue; await this.stateControl('Volume', scaledValue); if (this.logInfo) this.emit('info', `set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { state = state ? 'ON' : 'OFF'; await this.stateControl('Mute', state); if (this.logInfo) this.emit('info', `set Mute: ${state}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Mute error: ${error}`); } }); // lightbulb if (this.logDebug) this.emit('debug', `Prepare volume service lightbulb`); this.volumeServiceLightbulb = accessory.addService(Service.Lightbulb, volumeServiceName, 'Lightbulb Speaker'); this.volumeServiceLightbulb.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceLightbulb.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceLightbulb.getCharacteristic(Characteristic.Brightness) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Volume, value); }); this.volumeServiceLightbulb.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Mute, !state); }); break; case 5: // tv speaker + fan if (this.logDebug) this.emit('debug', `Prepare television speaker service`); this.volumeServiceTvSpeaker = accessory.addService(Service.TelevisionSpeaker, volumeServiceName, 'TV Speaker'); this.volumeServiceTvSpeaker.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.ConfiguredName, volumeServiceName); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Active) .onGet(async () => { const state = this.power; return state; }) .onSet(async (state) => { }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.VolumeControlType) .onGet(async () => { const state = 3; return state; }); this.volumeServiceTvSpeaker.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; } if (this.logInfo) this.emit('info', `set Volume Selector: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume Selector error: ${error}`); }; }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume) .onGet(async () => { const volume = this.volume; return volume; }) .onSet(async (value) => { try { value = value > this.volumeControlMax ? this.volumeControlMax : value; let scaledValue = Math.min(Math.round(value), 98); scaledValue = scaledValue < 10 ? `0${scaledValue}` : scaledValue; await this.stateControl('Volume', scaledValue); if (this.logInfo) this.emit('info', `set Volume: ${value}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Volume error: ${error}`); } }); this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Mute) .onGet(async () => { const state = this.mute; return state; }) .onSet(async (state) => { try { state = state ? 'ON' : 'OFF'; await this.stateControl('Mute', state); if (this.logInfo) this.emit('info', `set Mute: ${state}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Mute error: ${error}`); } }); // fan if (this.logDebug) this.emit('debug', `Prepare volume service fan`); this.volumeServiceFan = accessory.addService(Service.Fan, volumeServiceName, 'Fan Speaker'); 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.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Volume, value); }); this.volumeServiceFan.getCharacteristic(Characteristic.On) .onGet(async () => { const state = this.power ? !this.mute : false; return state; }) .onSet(async (state) => { this.volumeServiceTvSpeaker.setCharacteristic(Characteristic.Mute, !state); }); break; } } //prepare sensor service const possibleSensorCount = 99 - this.accessory.services.length; const maxSensorCount = this.sensors.length >= possibleSensorCount ? possibleSensorCount : this.sensors.length; if (maxSensorCount > 0) { this.sensorServices = []; if (this.logDebug) this.emit('debug', `Prepare inputs sensors services`); for (let i = 0; i < maxSensorCount; i++) { const sensor = this.sensors[i]; //get sensor name const name = sensor.name || `Sensor ${i}`; //get sensor name prefix const namePrefix = sensor.namePrefix; //get service type const serviceType = sensor.serviceType; //get characteristic type const characteristicType = sensor.characteristicType; const serviceName = namePrefix ? `${accessoryName} ${name}` : name; const sensorService = new serviceType(serviceName, `Sensor ${i}`); sensorService.addOptionalCharacteristic(Characteristic.ConfiguredName); sensorService.setCharacteristic(Characteristic.ConfiguredName, serviceName); sensorService.getCharacteristic(characteristicType) .onGet(async () => { const state = sensor.state; return state; }); this.sensorServices.push(sensorService); accessory.addService(sensorService); } } //prepare buttons services const possibleButtonsCount = 99 - this.accessory.services.length; const maxButtonsCount = this.buttons.length >= possibleButtonsCount ? possibleButtonsCount : this.buttons.length; if (maxButtonsCount > 0) { if (this.logDebug) this.emit('debug', `Prepare buttons services`); this.buttonsServices = []; for (let i = 0; i < maxButtonsCount; i++) { //get button const button = this.buttons[i]; //get button name const name = button.name || `Button ${i}`; //get button reference const reference = button.reference; //get button name prefix const namePrefix = button.namePrefix; //get service type const serviceType = button.serviceType; const serviceName = namePrefix ? `${accessoryName} ${name}` : name; 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 command = `Z3${reference.substring(1)}`; if (state) await this.denon.send(command); if (this.logInfo && state) this.emit('info', `set Button Name: ${name}, Reference: ${command}`); } catch (error) { if (this.logWarn) this.emit('warn', `set Button error: ${error}`); } }); this.buttonsServices.push(buttonService); accessory.addService(buttonService); } } return accessory; } catch (error) { throw new Error(error) }; } //start async start() { try { //denon client this.zone = new Zone(this.denon, this.device, this.inputsFile, this.restFul.enable, this.mqtt.enable) .on('deviceInfo', (info) => { this.emit('devInfo', `-------- ${this.name} --------`); this.emit('devInfo', `Manufacturer: ${info.manufacturer}`); this.emit('devInfo', `Model: ${info.modelName}`); this.emit('devInfo', `Control: ${info.controlZone}`); this.emit('devInfo', `----------------------------------`); this.informationService?.updateCharacteristic(Characteristic.FirmwareRevision, info.firmwareRevision); }) .on('addRemoveOrUpdateInput', async (inputs, remove) => { await this.addRemoveOrUpdateInput(inputs, remove); }) .on('stateChanged', async (power, reference, volume, volumeDisplay, mute, pictureMode) => { const input = this.inputsServices?.find(input => input.reference === reference); const inputIdentifier = input ? input.identifier : this.inputIdentifier; const scaledVolume = await this.functions.scaleValue(