homebridge-denon-tv
Version:
Homebridge plugin to control Denon/Marantz AV Receivers.
904 lines (830 loc) • 56.7 kB
JavaScript
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(