homebridge-lgwebos-tv
Version:
Homebridge plugin to control LG webOS TV.
870 lines (802 loc) • 101 kB
JavaScript
import EventEmitter from 'events';
import WakeOnLan from './wol.js';
import LgWebOsSocket from './lgwebossocket.js';
import Functions from './functions.js';
import { ApiUrls, SystemApps, PictureModes, SoundModes, SoundOutputs, PowerOnWaitAttempts } from './constants.js';
let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
class LgWebOsDevice extends EventEmitter {
constructor(api, device, keyFile, devInfoFile, inputsFile, channelsFile, 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.device = device;
this.name = device.name;
this.mac = device.mac;
this.displayType = device.displayType;
this.filterSystemApps = device.inputs?.filterSystemApps || false;
this.inputsDisplayOrder = device.inputs?.displayOrder || 0;
this.buttons = (device.buttons ?? []).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.startInput = device.power?.startInput || false;
this.startInputReference = device.power?.startInputReference || 'com.webos.app.home';
this.volumeControl = device.volume?.displayType || 0;
this.volumeControlName = device.volume?.name || 'Volume';
this.volumeControlNamePrefix = device.volume?.namePrefix || false;
this.soundModes = (device.sound?.modes ?? []).filter(mode => (mode.displayType ?? 0) > 0);
this.soundOutputs = (device.sound?.outputs ?? []).filter(output => (output.displayType ?? 0) > 0);
this.brightnessControl = device.picture?.brightnessControl || false;
this.backlightControl = device.picture?.backlightControl || false;
this.contrastControl = device.picture?.contrastControl || false;
this.colorControl = device.picture?.colorControl || false;
this.pictureModes = (device.picture?.modes ?? []).filter(mode => (mode.displayType ?? 0) > 0);
this.turnScreenOnOff = device.screen?.turnOnOff || false;
this.turnScreenSaverOnOff = device.screen?.saverOnOff || false;
this.disableTvService = device.disableTvService || false;
this.infoButtonCommand = device.infoButtonCommand || 'INFO';
this.logInfo = device.log?.info || false;
this.logWarn = device.log?.warn || true;
this.logError = device.log?.error || true;
this.logDebug = device.log?.debug || false;
//files
this.keyFile = keyFile;
this.devInfoFile = devInfoFile;
this.inputsFile = inputsFile;
this.channelsFile = channelsFile;
this.inputsNamesFile = inputsNamesFile;
this.inputsTargetVisibilityFile = inputsTargetVisibilityFile;
//external integrations
this.restFul = device.restFul ?? {};
this.restFul1 = restFul1;
this.restFulConnected = restFulConnected;
this.mqtt = device.mqtt ?? {};
this.mqtt1 = mqtt1;
this.mqttConnected = mqttConnected;
//state variables
this.functions = new Functions();
this.inputIdentifier = 1;
this.reference = null;
this.power = false;
this.pixelRefreshState = false;
this.screenStateOff = false;
this.screenSaverState = false;
this.volume = 0;
this.mute = false;
this.appType = '';
this.playState = false; // fix #10: was 'plyState'
this.channelId = 0;
this.channelName = '';
this.channelNumber = 0;
this.brightness = 0;
this.backlight = 0;
this.contrast = 0;
this.color = 0;
this.pictureModeHomeKit = 1;
this.pictureMode = '';
this.soundMode = '';
this.soundOutput = '';
this.isBooting = false;
this.screenOff = false;
this.screenSaver = false;
this.activeStandby = false;
//picture mode variable
for (const mode of this.pictureModes) {
mode.serviceType = [null, Service.Outlet, Service.Switch][mode.displayType];
mode.state = false;
}
//sound mode variable
for (const mode of this.soundModes) {
mode.serviceType = [null, Service.Outlet, Service.Switch][mode.displayType];
mode.state = false;
}
//sound output variable
for (const output of this.soundOutputs) {
output.serviceType = [null, Service.Outlet, Service.Switch][output.displayType];
output.state = false;
}
//sensors variable
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 variable
for (const button of this.buttons) {
button.reference = [button.reference, button.reference, button.command][button.mode];
button.serviceType = [null, Service.Outlet, Service.Switch][button.displayType];
button.state = false;
}
}
async setOverExternalIntegration(integration, key, value) {
try {
let set = false;
let payload = {};
let cid;
switch (key) {
case 'Power':
switch (value) {
case true:
set = !this.power ? await this.wol.wakeOnLan() : true;
break;
case false:
cid = await this.lgWebOsSocket.getCid('Power');
set = this.power ? await this.lgWebOsSocket.send('request', ApiUrls.TurnOff, undefined, cid) : true;
break;
}
break;
case 'App':
cid = await this.lgWebOsSocket.getCid('App');
set = await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, { id: value }, cid);
break;
case 'Channel':
cid = await this.lgWebOsSocket.getCid('Channel');
set = await this.lgWebOsSocket.send('request', ApiUrls.OpenChannel, { channelId: value }, cid);
break;
case 'Input':
cid = await this.lgWebOsSocket.getCid('App');
set = await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, { id: value }, cid);
break;
case 'Volume': {
const volume = (value < 0 || value > 100) ? this.volume : value;
payload = { volume };
cid = await this.lgWebOsSocket.getCid('Audio');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid);
break;
}
case 'Mute':
payload = { mute: value };
cid = await this.lgWebOsSocket.getCid('Audio');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetMute, payload, cid);
break;
case 'Brightness':
payload = { category: 'picture', settings: { brightness: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'Backlight':
payload = { category: 'picture', settings: { backlight: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'Contrast':
payload = { category: 'picture', settings: { contrast: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'Color':
payload = { category: 'picture', settings: { color: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'PictureMode':
payload = { category: 'picture', settings: { pictureMode: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'SoundMode':
payload = { category: 'sound', settings: { soundMode: value } };
cid = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid);
break;
case 'SoundOutput':
payload = { output: value };
cid = await this.lgWebOsSocket.getCid('SoundOutput');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetSoundOutput, payload, cid);
break;
case 'PlayState':
payload = { name: value ? 'PLAY' : 'PAUSE' };
set = await this.lgWebOsSocket.send('button', undefined, payload);
break;
case 'RcControl':
payload = { name: value };
set = await this.lgWebOsSocket.send('button', undefined, payload);
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)}`);
this.webOS = this.savedInfo.webOS ?? 2.0;
//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 channels from file
this.savedChannels = await this.functions.readData(this.channelsFile, true) ?? [];
if (this.logDebug) this.emit('debug', `Read saved Channels: ${JSON.stringify(this.savedChannels, 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/Channels 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/Channels Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`);
return true;
} catch (error) {
throw new Error(`Prepare data for accessory error: ${error}`);
}
}
async startStopImpulseGenerator(state, timers = []) {
try {
await this.lgWebOsSocket.impulseGenerator.state(state, timers);
return true;
} catch (error) {
throw new Error(`Impulse generator start 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];
if (sortFn) {
this.inputsServices.sort(sortFn);
}
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)}`);
}
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 visible = input.visible ?? true;
const systemApp = this.filterSystemApps && SystemApps.includes(input.reference);
if (systemApp) 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 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.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);
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}`);
}
});
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;
}
}
if (updated) await this.displayOrder();
return true;
} catch (error) {
throw new Error(`Add/Remove/Update input error: ${error}`);
}
}
async setInput(input) {
try {
const { mode, name, reference } = input;
let cid = await this.lgWebOsSocket.getCid('App');
let payload = {};
switch (mode) {
case 0: // App/Input Source
switch (reference) {
case 'com.webos.app.screensaver':
cid = await this.lgWebOsSocket.getCid();
await this.lgWebOsSocket.send('alert', ApiUrls.TurnOnScreenSaver, undefined, cid, 'Screen Saver', 'ON');
break;
case 'com.webos.app.screenoff': {
const url = this.webOS >= 4.0 ? this.webOS >= 4.5 ? ApiUrls.TurnOffScreen45 : ApiUrls.TurnOffScreen : false;
cid = await this.lgWebOsSocket.getCid('Power');
await this.lgWebOsSocket.send('request', url, undefined, cid);
break;
}
case 'com.webos.app.factorywin':
payload = {
id: ApiUrls.ServiceMenu,
params: { id: 'executeFactory', irKey: 'inStart' }
};
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
break;
default: {
payload = { id: reference };
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
break;
}
}
break;
case 1: { // Channel
const liveTv = 'com.webos.app.livetv';
if (this.reference !== liveTv) await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, { id: liveTv }, cid);
cid = await this.lgWebOsSocket.getCid('Channel');
await this.lgWebOsSocket.send('request', ApiUrls.OpenChannel, { channelId: reference }, cid);
break;
}
default:
if (this.logWarn) this.emit('warn', `Set Input not possible, unknown mode: ${mode}`);
return;
}
if (this.logInfo) this.emit('info', `Set ${mode === 0 ? 'Input' : 'Channel'}, Name: ${name}, Reference: ${reference}`);
return;
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Input or Channel error: ${error}`);
}
}
//prepare accessory
async prepareAccessory() {
try {
if (this.logDebug) this.emit('debug', `Prepare accessory`);
const accessoryName = this.name;
const accessoryUUID = AccessoryUUID.generate(this.mac);
const accessoryCategory = [Categories.OTHER, Categories.TELEVISION, Categories.TV_SET_TOP_BOX, Categories.TV_STREAMING_STICK, Categories.AUDIO_RECEIVER][this.displayType];
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 ?? 'LG Electronics')
.setCharacteristic(Characteristic.Model, this.savedInfo.modelName ?? 'Model Name')
.setCharacteristic(Characteristic.SerialNumber, this.savedInfo.deviceId ?? 'Serial Number')
.setCharacteristic(Characteristic.FirmwareRevision, this.savedInfo.firmwareRevision ?? 'Firmware Revision')
.setCharacteristic(Characteristic.ConfiguredName, accessoryName);
//prepare television service
if (!this.disableTvService) {
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) => {
const requestedPower = state === 1;
if (requestedPower === this.power && !this.isBooting) return;
try {
switch (state) {
case 1:
this.isBooting = true;
await this.wol.wakeOnLan();
if (this.startInput) {
(async () => {
try {
for (let attempt = 0; attempt < PowerOnWaitAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, 1000));
if (this.power) {
if (this.inputIdentifier !== this.startInputReference) {
const cid = await this.lgWebOsSocket.getCid('App');
const payload = { id: this.startInputReference };
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
}
return;
}
}
} finally {
this.isBooting = false;
}
})();
} else {
this.isBooting = false;
}
break;
case 0: {
const cid = await this.lgWebOsSocket.getCid('Power');
await this.lgWebOsSocket.send('request', ApiUrls.TurnOff, undefined, cid);
break;
}
}
if (this.logInfo) this.emit('info', `Set Power: ${state ? 'ON' : 'OFF'}`);
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Power error: ${error}`);
}
});
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
.onGet(async () => {
const inputIdentifier = this.inputIdentifier; // fix #1: removed duplicate semicolon
return inputIdentifier;
})
.onSet(async (activeIdentifier) => {
const input = this.inputsServices.find(input => input.identifier === activeIdentifier);
if (!input) {
if (this.logWarn) this.emit('warn', `Input with identifier ${activeIdentifier} not found`);
return;
}
try {
if (!this.power && !this.startInput) {
if (this.logDebug) this.emit('debug', `Device is off, deferring input switch to '${activeIdentifier}'`);
(async () => {
for (let attempt = 0; attempt < PowerOnWaitAttempts; attempt++) {
await new Promise(resolve => setTimeout(resolve, 1000));
if (this.power && !this.isBooting) {
if (this.inputIdentifier !== activeIdentifier) {
if (this.logDebug) this.emit('debug', `Retrying channel switch (${attempt + 1}/${PowerOnWaitAttempts})`);
await this.setInput(input);
} else {
this.televisionService.updateCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier);
if (this.logInfo) this.emit('info', `Channel set successfully: ${input.name}`);
return;
}
}
}
if (this.logWarn) this.emit('warn', `Failed to set channel after retries: ${input.name}`);
})().catch(err => {
if (this.logWarn) this.emit('warn', `Set channel retry error: ${err}`);
});
return;
}
await this.setInput(input);
if (this.logInfo) this.emit('info', `Set Channel Name: ${input.name}, Reference: ${input.reference}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Input error: ${JSON.stringify(error, null, 2)}`);
}
});
this.televisionService.getCharacteristic(Characteristic.RemoteKey)
.onSet(async (command) => {
try {
switch (command) {
case Characteristic.RemoteKey.REWIND: command = 'REWIND'; break;
case Characteristic.RemoteKey.FAST_FORWARD: command = 'FASTFORWARD'; break;
case Characteristic.RemoteKey.NEXT_TRACK: command = 'GOTONEXT'; break;
case Characteristic.RemoteKey.PREVIOUS_TRACK: command = 'GOTOPREV'; break;
case Characteristic.RemoteKey.ARROW_UP: command = 'UP'; break;
case Characteristic.RemoteKey.ARROW_DOWN: command = 'DOWN'; break;
case Characteristic.RemoteKey.ARROW_LEFT: command = 'LEFT'; break;
case Characteristic.RemoteKey.ARROW_RIGHT: command = 'RIGHT'; break;
case Characteristic.RemoteKey.SELECT: command = 'ENTER'; break;
case Characteristic.RemoteKey.BACK: command = 'BACK'; break;
case Characteristic.RemoteKey.EXIT: command = 'EXIT'; break;
case Characteristic.RemoteKey.PLAY_PAUSE:
command = this.playState ? 'PAUSE' : 'PLAY'; // fix #10: was this.plyState
break;
case Characteristic.RemoteKey.INFORMATION: command = this.infoButtonCommand; break;
}
const payload = { name: command };
await this.lgWebOsSocket.send('button', undefined, payload);
if (this.logInfo) this.emit('info', `Set Remote Key: ${command}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Remote Key error: ${error}`);
}
});
//optional television characteristics
this.televisionService.getCharacteristic(Characteristic.ClosedCaptions)
.onGet(async () => {
const state = 0;
return state;
})
.onSet(async (state) => {
try {
if (this.logInfo) this.emit('info', `Set Closed Captions: ${state}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Closed Captions error: ${error}`);
}
});
this.televisionService.getCharacteristic(Characteristic.CurrentMediaState)
.onGet(async () => {
//0 - PLAY, 1 - PAUSE, 2 - STOP, 3 - LOADING, 4 - INTERRUPTED
const value = 2;
return value;
});
this.televisionService.getCharacteristic(Characteristic.TargetMediaState)
.onGet(async () => {
//0 - PLAY, 1 - PAUSE, 2 - STOP
const value = 2;
return value;
})
.onSet(async (value) => {
try {
const newMediaState = [ApiUrls.SetMediaPlay, ApiUrls.SetMediaPause, ApiUrls.SetMediaStop][value];
await this.lgWebOsSocket.send('request', newMediaState);
if (this.logInfo) this.emit('info', `Set Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Media error: ${error}`);
}
});
this.televisionService.getCharacteristic(Characteristic.PowerModeSelection)
.onSet(async (command) => {
try {
switch (command) {
case Characteristic.PowerModeSelection.SHOW: command = 'MENU'; break;
case Characteristic.PowerModeSelection.HIDE: command = 'BACK'; break;
}
const payload = { name: command };
await this.lgWebOsSocket.send('button', undefined, payload);
if (this.logInfo) this.emit('info', `Set Power Mode Selection: ${command === 'MENU' ? 'SHOW' : 'HIDE'}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Power Mode Selection error: ${error}`);
}
});
if (this.webOS >= 4.0) {
this.televisionService.getCharacteristic(Characteristic.Brightness)
.onGet(async () => {
const brightness = this.brightness;
return brightness;
})
.onSet(async (value) => {
try {
const payload = {
category: 'picture',
settings: { brightness: value }
};
const cid = await this.lgWebOsSocket.getCid();
await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid, 'Set Brightness', `Value: ${value}`);
if (this.logInfo) this.emit('info', `Set Brightness: ${value}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Brightness error: ${error}`);
}
});
this.televisionService.getCharacteristic(Characteristic.PictureMode)
.onGet(async () => {
const value = this.pictureModeHomeKit;
return value;
})
.onSet(async (command) => {
try {
switch (command) {
case Characteristic.PictureMode.OTHER: command = 'cinema'; break;
case Characteristic.PictureMode.STANDARD: command = 'normal'; break;
case Characteristic.PictureMode.CALIBRATED: command = 'expert1'; break;
case Characteristic.PictureMode.CALIBRATED_DARK: command = 'expert2'; break;
case Characteristic.PictureMode.VIVID: command = 'vivid'; break;
case Characteristic.PictureMode.GAME: command = 'game'; break;
case Characteristic.PictureMode.COMPUTER: command = 'photo'; break;
case Characteristic.PictureMode.CUSTOM: command = 'sport'; break;
}
const payload = {
category: 'picture',
settings: { pictureMode: command }
};
const cid = await this.lgWebOsSocket.getCid();
await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload, cid, 'Set Picture Mode', `Value: ${PictureModes[command] ?? 'Unknown'}`);
if (this.logInfo) this.emit('info', `Set Picture Mode: ${PictureModes[command] ?? 'Unknown'}`);
} catch (error) {
if (this.logWarn) this.emit('warn', `Set Picture Mode error: ${error}`);
}
});
}
//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 {
const payload = { volume: value };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid);
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 {
const payload = { mute: !state };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetMute, payload, cid);
if (this.logInfo) this.emit('info', `Set Mute: ${!state ? 'ON' : 'OFF'}`);
} 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 {
const payload = { volume: value };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid);
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 {
const payload = { mute: !state };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetMute, payload, cid);
if (this.logInfo) this.emit('info', `Set Mute: ${!state ? 'ON' : 'OFF'}`);
} 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`);
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 = 'VOLUMEUP'; break;
case Characteristic.VolumeSelector.DECREMENT: command = 'VOLUMEDOWN'; break;
}
const payload = { name: command };
await this.lgWebOsSocket.send('button', undefined, payload);
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 {
const payload = { volume: value };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid);
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 {
const payload = { mute: state };
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetMute, payload, cid);
if (this.logInfo) this.emit('info', `Set Mute: ${state ? 'ON' : 'OFF'}`);
} 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 = 'VOLUMEUP'; break;
case Characteristic.VolumeSelector.DECREMENT: command = 'VOLUMEDOWN'; break;
}
const payload = { name: command };
await this.lgWebOsSocket.send('button', undefined, payload);
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.getChara