homebridge-lgwebos-tv
Version:
Homebridge plugin to control LG webOS TV.
958 lines (882 loc) • 118 kB
JavaScript
import { promises as fsPromises } from 'fs';
import EventEmitter from 'events';
import RestFul from './restful.js';
import Mqtt from './mqtt.js';
import WakeOnLan from './wol.js';
import LgWebOsSocket from './lgwebossocket.js';
import { ApiUrls, DefaultInputs, PictureModes, SoundModes, SoundOutputs } from './constants.js';
let Accessory, Characteristic, Service, Categories, Encode, AccessoryUUID;
class LgWebOsDevice extends EventEmitter {
constructor(api, device, keyFile, devInfoFile, inputsFile, channelsFile, inputsNamesFile, inputsTargetVisibilityFile) {
super();
Accessory = api.platformAccessory;
Characteristic = api.hap.Characteristic;
Service = api.hap.Service;
Categories = api.hap.Categories;
Encode = api.hap.encode;
AccessoryUUID = api.hap.uuid;
//device configuration
this.name = device.name;
this.host = device.host;
this.mac = device.mac;
this.broadcastAddress = device.broadcastAddress || '255.255.255.255';
this.getInputsFromDevice = device.getInputsFromDevice || false;
this.filterSystemApps = this.getInputsFromDevice ? device.filterSystemApps : false;
this.disableLoadDefaultInputs = device.disableLoadDefaultInputs || false;
this.inputs = device.inputs || [];
this.inputsDisplayOrder = device.inputsDisplayOrder || 0;
this.buttons = device.buttons || [];
this.sensorPower = device.sensorPower || false;
this.sensorPixelRefresh = device.sensorPixelRefresh || false;
this.sensorVolume = device.sensorVolume || false;
this.sensorMute = device.sensorMute || false;
this.sensorInput = device.sensorInput || false;
this.sensorChannel = device.sensorChannel || false;
this.sensorSoundMode = device.sensorSoundMode || false;
this.sensorSoundOutput = device.sensorSoundOutput || false;
this.sensorPictureMode = device.sensorPictureMode || false;
this.sensorScreenOnOff = device.sensorScreenOnOff || false;
this.sensorScreenSaver = device.sensorScreenSaver || false;
this.sensorPlayState = device.sensorPlayState || false;
this.sensorInputs = device.sensorInputs || [];
this.brightnessControl = device.brightnessControl || false;
this.backlightControl = device.backlightControl || false;
this.contrastControl = device.contrastControl || false;
this.colorControl = device.colorControl || false;
this.pictureModeControl = device.pictureModeControl || false;
this.pictureModes = this.pictureModeControl ? device.pictureModes || [] : [];
this.soundModeControl = device.soundModeControl || false;
this.soundModes = this.soundModeControl ? device.soundModes || [] : [];
this.soundOutputControl = device.soundOutputControl || false;
this.soundOutputs = this.soundOutputControl ? device.soundOutputs || [] : [];
this.serviceMenu = device.serviceMenu || false;
this.ezAdjustMenu = device.ezAdjustMenu || false;
this.disableTvService = device.disableTvService || false;
this.turnScreenOnOff = device.turnScreenOnOff || false;
this.turnScreenSaverOnOff = device.turnScreenSaverOnOff || false;
this.sslWebSocket = device.sslWebSocket || false;
this.infoButtonCommand = device.infoButtonCommand || 'INFO';
this.volumeControlNamePrefix = device.volumeControlNamePrefix || false;
this.volumeControlName = device.volumeControlName || 'Volume';
this.volumeControl = device.volumeControl || false;
this.enableDebugMode = device.enableDebugMode || false;
this.disableLogInfo = device.disableLogInfo || false;
this.keyFile = keyFile;
this.devInfoFile = devInfoFile;
this.inputsFile = inputsFile;
this.channelsFile = channelsFile;
this.inputsNamesFile = inputsNamesFile;
this.inputsTargetVisibilityFile = inputsTargetVisibilityFile;
this.startPrepareAccessory = true;
//external integrations
this.restFul = device.restFul ?? {};
this.restFulConnected = false;
this.mqtt = device.mqtt ?? {};
this.mqttConnected = false;
//accessory services
this.allServices = [];
//add configured inputs to the default inputs
this.inputs = this.disableLoadDefaultInputs ? this.inputs : [...DefaultInputs, ...this.inputs];
this.inputsConfigured = [];
this.inputIdentifier = 1;
//state variable
this.power = false;
this.pixelRefreshState = false;
this.screenStateOff = false;
this.screenSaverState = false;
this.appId = '';
this.volume = 0;
this.mute = false;
this.playState = false;
this.appType = '';
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.invertMediaState = false;
//picture mode variable
this.picturesModesConfigured = [];
for (const mode of this.pictureModes) {
const pictureModeName = mode.name ?? false;
const pictureModeReference = mode.reference ?? false;
const pictureModeDisplayType = mode.displayType ?? 0;
if (pictureModeName && pictureModeReference && pictureModeDisplayType > 0) {
mode.serviceType = ['', Service.Outlet, Service.Switch][pictureModeDisplayType];
mode.state = false;
this.picturesModesConfigured.push(mode);
} else {
const log = pictureModeDisplayType === 0 ? false : this.emit('info', `Picture Mode Name: ${pictureModeName ? pictureModeName : 'Missing'}, 'Reference: ${pictureModeReference ? pictureModeReference : 'Missing'}`);
};
}
this.picturesModesConfiguredCount = this.picturesModesConfigured.length || 0;
//sound mode variable
this.soundsModesConfigured = [];
for (const mode of this.soundModes) {
const soundModeName = mode.name ?? false;
const soundModeReference = mode.reference ?? false;
const soundModeDisplayType = mode.displayType ?? 0;
if (soundModeName && soundModeReference && soundModeDisplayType > 0) {
mode.serviceType = ['', Service.Outlet, Service.Switch][soundModeDisplayType];
mode.state = false;
this.soundsModesConfigured.push(mode);
} else {
const log = soundModeDisplayType === 0 ? false : this.emit('info', `Sound Mode Name: ${soundModeName ? soundModeName : 'Missing'}, 'Reference: ${soundModeReference ? soundModeReference : 'Missing'}`);
};
}
this.soundsModesConfiguredCount = this.soundsModesConfigured.length || 0;
//sound output variable
this.soundsOutputsConfigured = [];
for (const output of this.soundOutputs) {
const soundOutputName = output.name ?? false;
const soundOutputReference = output.reference ?? false;
const soundOutputDisplayType = output.displayType ?? 0;
if (soundOutputName && soundOutputReference && soundOutputDisplayType > 0) {
output.serviceType = ['', Service.Outlet, Service.Switch][soundOutputDisplayType];
output.state = false;
this.soundsOutputsConfigured.push(output);
} else {
const log = soundOutputDisplayType === 0 ? false : this.emit('info', `Sound Mode Name: ${soundOutputName ? soundOutputName : 'Missing'}, 'Reference: ${soundOutputReference ? soundOutputReference : 'Missing'}`);
};
}
this.soundsOutputsConfiguredCount = this.soundsOutputsConfigured.length || 0;
//sensors variable
this.sensorsInputsConfigured = [];
for (const sensor of this.sensorInputs) {
const sensorInputName = sensor.name ?? false;
const sensorInputReference = sensor.reference ?? false;
const sensorInputDisplayType = sensor.displayType ?? 0;
if (sensorInputName && sensorInputReference && sensorInputDisplayType > 0) {
sensor.serviceType = ['', Service.MotionSensor, Service.OccupancySensor, Service.ContactSensor][sensorInputDisplayType];
sensor.characteristicType = ['', Characteristic.MotionDetected, Characteristic.OccupancyDetected, Characteristic.ContactSensorState][sensorInputDisplayType];
sensor.state = false;
this.sensorsInputsConfigured.push(sensor);
} else {
const log = sensorInputDisplayType === 0 ? false : this.emit('info', `Sensor Name: ${sensorInputName ? sensorInputName : 'Missing'}, Reference: ${sensorInputReference ? sensorInputReference : 'Missing'}`);
};
}
this.sensorsInputsConfiguredCount = this.sensorsInputsConfigured.length || 0;
this.sensorVolumeState = false;
this.sensorInputState = false;
this.sensorChannelState = false;
this.sensorSoundModeState = false;
this.sensorSoundOutputState = false;
this.sensorPicturedModeState = false;
//buttons variable
this.buttonsConfigured = [];
for (const button of this.buttons) {
const buttonName = button.name ?? false;
const buttonMode = button.mode ?? -1;
const buttonReferenceCommand = [button.reference, button.reference, button.command][buttonMode] ?? false;
const buttonDisplayType = button.displayType ?? 0;
if (buttonName && buttonMode >= 0 && buttonReferenceCommand && buttonDisplayType > 0) {
button.serviceType = ['', Service.Outlet, Service.Switch][buttonDisplayType];
button.state = false;
this.buttonsConfigured.push(button);
} else {
const log = buttonDisplayType === 0 ? false : this.emit('info', `Button Name: ${buttonName ? buttonName : 'Missing'}, ${buttonMode ? 'Command:' : 'Reference:'} ${buttonReferenceCommand ? buttonReferenceCommand : 'Missing'}, Mode: ${buttonMode ? buttonMode : 'Missing'}`);
};
}
this.buttonsConfiguredCount = this.buttonsConfigured.length || 0;
}
async saveData(path, data) {
try {
data = JSON.stringify(data, null, 2);
await fsPromises.writeFile(path, data);
const debug = !this.enableDebugMode ? false : this.emit('debug', `Saved data: ${data}`);
return true;
} catch (error) {
throw new Error(`Save data error: ${error}`);
}
}
async readData(path) {
try {
const data = await fsPromises.readFile(path);
return data;
} catch (error) {
throw new Error(`Read data error: ${error}`);
}
}
async sanitizeString(str) {
// Replace dots, colons, and semicolons inside words with a space
str = str.replace(/(\w)[.:;]+(\w)/g, '$1 $2');
// Remove remaining dots, colons, semicolons, plus, and minus anywhere in the string
str = str.replace(/[.:;+\-]/g, '');
// Replace all other invalid characters (anything not A-Z, a-z, 0-9, space, or apostrophe) with a space
str = str.replace(/[^A-Za-z0-9 ']/g, ' ');
// Trim leading and trailing spaces
str = str.trim();
return str;
}
async setOverExternalIntegration(integration, key, value) {
try {
let set = false
switch (key) {
case 'Power':
switch (value) {
case true:
set = !this.power ? await this.wol.wakeOnLan() : true;
break;
case false:
const cid = await this.lgWebOsSocket.getCid('Power');
set = this.power ? await this.lgWebOsSocket.send('request', ApiUrls.TurnOff, undefined, cid) : true;
break;
}
break;
case 'App':
const cid = await this.lgWebOsSocket.getCid('App');
set = await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, { id: value }, cid);
break;
case 'Channel':
const cid0 = await this.lgWebOsSocket.getCid('Channel');
set = await this.lgWebOsSocket.send('request', ApiUrls.OpenChannel, { channelId: value }, cid0)
break;
case 'Input':
const cid1 = await this.lgWebOsSocket.getCid('App');
set = await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, { id: value }, cid1);
break;
case 'Volume':
const volume = (value < 0 || value > 100) ? this.volume : value;
const payload = {
volume: volume
};
const cid2 = await this.lgWebOsSocket.getCid('Audio');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid2);
break;
case 'Mute':
const payload3 = {
mute: value
};
const cid3 = await this.lgWebOsSocket.getCid('Audio');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetMute, payload3, cid3);
break;
case 'Brightness':
const payload4 = {
category: 'picture',
settings: {
brightness: value
}
};
const cid4 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload4, cid4);
break;
case 'Backlight':
const payload5 = {
category: 'picture',
settings: {
backlight: value
}
};
const cid5 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload5, cid5);
break;
case 'Contrast':
const payload6 = {
category: 'picture',
settings: {
contrast: value
}
};
const cid6 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload6, cid6);
break;
case 'Color':
const payload7 = {
category: 'picture',
settings: {
color: value
}
};
const cid7 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload7, cid7);
break;
case 'PictureMode':
const payload8 = {
category: 'picture',
settings: {
pictureMode: value
}
};
const cid8 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload8, cid8);
break;
case 'SoundMode':
const payload9 = {
category: 'sound',
settings: {
soundMode: value
}
};
const cid9 = await this.lgWebOsSocket.getCid();
set = await this.lgWebOsSocket.send('alert', ApiUrls.SetSystemSettings, payload9, cid9);
break;
case 'SoundOutput':
const payload10 = {
output: value
};
const cid10 = await this.lgWebOsSocket.getCid('SoundOutput');
set = await this.lgWebOsSocket.send('request', ApiUrls.SetSoundOutput, payload10, cid10);
break;
case 'PlayState':
const payload11 = {
playState: value
};
const cid11 = await this.lgWebOsSocket.getCid('MediaInfo');
set = await this.lgWebOsSocket.send('request', ApiUrls.GetForegroundAppMediaInfo, payload11, cid11);
break;
case 'RcControl':
const payload12 = {
name: value
};
set = await this.lgWebOsSocket.send('button', undefined, payload12);
break;
default:
this.emit('warn', `${integration}, received key: ${key}, value: ${value}`);
break;
};
return set;
} catch (error) {
throw new Error(`${integration} set key: ${key}, value: ${value}, error: ${error}`);
}
}
async externalIntegrations() {
try {
//RESTFul server
const restFulEnabled = this.restFul.enable || false;
if (restFulEnabled) {
this.restFul1 = new RestFul({
port: this.restFul.port || 3000,
debug: this.restFul.debug || false
});
this.restFul1.on('connected', (message) => {
this.emit('success', message);
this.restFulConnected = true;
})
.on('set', async (key, value) => {
try {
await this.setOverExternalIntegration('RESTFul', key, value);
} catch (error) {
this.emit('warn', `RESTFul set error: ${error}`);
}
})
.on('debug', (debug) => {
this.emit('debug', debug);
})
.on('warn', (warn) => {
this.emit('warn', warn);
})
.on('error', (error) => {
this.emit('error', error);
});
}
//mqtt client
const mqttEnabled = this.mqtt.enable || false;
if (mqttEnabled) {
this.mqtt1 = new Mqtt({
host: this.mqtt.host,
port: this.mqtt.port || 1883,
clientId: this.mqtt.clientId ? `lg_${this.mqtt.clientId}_${Math.random().toString(16).slice(3)}` : `lg_${Math.random().toString(16).slice(3)}`,
prefix: this.mqtt.prefix ? `lg/${this.mqtt.prefix}/${this.name}` : `lg/${this.name}`,
user: this.mqtt.user,
passwd: this.mqtt.passwd,
debug: this.mqtt.debug || false
})
this.mqtt1.on('connected', (message) => {
this.emit('success', message);
this.mqttConnected = true;
})
.on('subscribed', (message) => {
this.emit('success', message);
})
.on('set', async (key, value) => {
try {
await this.setOverExternalIntegration('MQTT', key, value);
} catch (error) {
this.emit('warn', `MQTT set error: ${error}`);
}
})
.on('debug', (debug) => {
this.emit('debug', debug);
})
.on('warn', (warn) => {
this.emit('warn', warn);
})
.on('error', (error) => {
this.emit('error', error);
});
}
return true;
} catch (error) {
this.emit('warn', `External integration start error: ${error}`);
}
}
async prepareDataForAccessory() {
try {
//read dev info from file
const savedInfo = await this.readData(this.devInfoFile);
this.savedInfo = savedInfo.toString().trim() !== '' ? JSON.parse(savedInfo) : {};
const debug = this.enableDebugMode ? this.emit('debug', `Read saved Info: ${JSON.stringify(this.savedInfo, null, 2)}`) : false;
this.webOS = this.savedInfo.webOS ?? 2.0;
//read inputs file
const savedInputs = await this.readData(this.inputsFile);
this.savedInputs = savedInputs.toString().trim() !== '' ? JSON.parse(savedInputs) : this.inputs;
const debug1 = this.enableDebugMode ? this.emit('debug', `Read saved Inputs: ${JSON.stringify(this.savedInputs, null, 2)}`) : false;
//read channels from file
const savedChannels = await this.readData(this.channelsFile);
this.savedChannels = savedChannels.toString().trim() !== '' ? JSON.parse(savedChannels) : [];
const debug2 = this.enableDebugMode ? this.emit('debug', `Read saved Channels: ${JSON.stringify(this.savedChannels, null, 2)}`) : false;
//read inputs names from file
const savedInputsNames = await this.readData(this.inputsNamesFile);
this.savedInputsNames = savedInputsNames.toString().trim() !== '' ? JSON.parse(savedInputsNames) : {};
const debug3 = this.enableDebugMode ? this.emit('debug', `Read saved Inputs/Channels Names: ${JSON.stringify(this.savedInputsNames, null, 2)}`) : false;
//read inputs visibility from file
const savedInputsTargetVisibility = await this.readData(this.inputsTargetVisibilityFile);
this.savedInputsTargetVisibility = savedInputsTargetVisibility.toString().trim() !== '' ? JSON.parse(savedInputsTargetVisibility) : {};
const debug4 = this.enableDebugMode ? this.emit('debug', `Read saved Inputs/Channels Target Visibility: ${JSON.stringify(this.savedInputsTargetVisibility, null, 2)}`) : false;
return true;
} catch (error) {
throw new Error(`Prepare data for accessory error: ${error}`);
}
}
async startImpulseGenerator() {
try {
//start impulse generator
await new Promise(resolve => setTimeout(resolve, 3000));
await this.lgWebOsSocket.impulseGenerator.start([{ name: 'heartBeat', sampling: 10000 }]);
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) return;
this.inputsConfigured.sort(sortFn);
if (this.enableDebugMode) {
this.emit('debug', `Inputs display order:\n${JSON.stringify(this.inputsConfigured, null, 2)}`);
}
const displayOrder = this.inputsConfigured.map(input => input.identifier);
const encodedOrder = Encode(1, displayOrder).toString('base64');
this.televisionService.setCharacteristic(Characteristic.DisplayOrder, encodedOrder);
} catch (error) {
throw new Error(`Display order error: ${error}`);
}
}
//prepare accessory
async prepareAccessory() {
try {
//accessory
const debug = this.enableDebugMode ? this.emit('debug', `Prepare accessory`) : false;
const accessoryName = this.name;
const accessoryUUID = AccessoryUUID.generate(this.mac);
const accessoryCategory = Categories.TELEVISION;
const accessory = new Accessory(accessoryName, accessoryUUID, accessoryCategory);
//information service
const debug1 = this.enableDebugMode ? this.emit('debug', `Prepare information service`) : false;
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);
this.allServices.push(this.informationService);
//prepare television service
if (!this.disableTvService) {
const debug2 = this.enableDebugMode ? this.emit('debug', `Prepare television service`) : false;
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 {
switch (state) {
case 1:
await this.wol.wakeOnLan();
break;
case 0:
const cid = await this.lgWebOsSocket.getCid('Power');
await this.lgWebOsSocket.send('request', ApiUrls.TurnOff, undefined, cid);
break;
}
const info = this.disableLogInfo ? false : this.emit('info', `set Power: ${state ? 'ON' : 'OFF'}`);
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
this.emit('warn', `set Power error: ${error}`);
}
});
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
.onGet(async () => {
return this.inputIdentifier;
})
.onSet(async (activeIdentifier) => {
try {
const input = this.inputsConfigured.find(i => i.identifier === activeIdentifier);
if (!input) {
this.emit('warn', `Input with identifier ${activeIdentifier} not found`);
return;
}
const { mode: inputMode, name: inputName, reference: inputReference } = input;
if (!this.power) {
// Schedule retry attempts without blocking Homebridge
this.emit('debug', `TV is off, deferring input switch to '${activeIdentifier}'`);
(async () => {
for (let attempt = 0; attempt < 15; attempt++) {
await new Promise(resolve => setTimeout(resolve, 1500));
if (this.power && this.inputIdentifier !== activeIdentifier) {
this.emit('debug', `TV powered on, retrying input switch`);
this.televisionService.setCharacteristic(Characteristic.ActiveIdentifier, activeIdentifier);
break;
}
}
})();
return;
}
let cid = await this.lgWebOsSocket.getCid('App');
switch (inputMode) {
case 0: // App/Input Source
switch (inputReference) {
case 'com.webos.app.screensaver':
cid = await this.lgWebOsSocket.getCid();
await this.lgWebOsSocket.send('alert', ApiUrls.TurnOnScreenSaver, undefined, cid, 'Screen Saver', 'ON');
break;
case 'service.menu': {
const payload = {
id: ApiUrls.ServiceMenu,
params: { id: 'executeFactory', irKey: 'inStart' }
};
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
break;
}
case 'ez.adjust': {
const payload = {
id: ApiUrls.ServiceMenu,
params: { id: 'executeFactory', irKey: 'ezAdjust' }
};
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
break;
}
default: {
const payload = { id: inputReference };
await this.lgWebOsSocket.send('request', ApiUrls.LaunchApp, payload, cid);
break;
}
}
break;
case 1: // Channel
const liveTv = 'com.webos.app.livetv';
if (this.appId !== 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: inputReference }, cid);
break;
}
if (!this.disableLogInfo) {
this.emit('info', `set ${inputMode === 0 ? 'Input' : 'Channel'}, Name: ${inputName}, Reference: ${inputReference}`);
}
} catch (error) {
this.emit('warn', `set Input or Channel error: ${error}`);
}
});
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.invertMediaState ? 'PLAY' : 'PAUSE';
this.invertMediaState = !this.invertMediaState;
break;
case Characteristic.RemoteKey.INFORMATION:
command = this.infoButtonCommand;
break;
}
const payload = {
name: command
};
await this.lgWebOsSocket.send('button', undefined, payload);
const info = this.disableLogInfo ? false : this.emit('info', `set Remote Key: ${command}`);
} catch (error) {
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 {
const info = this.disableLogInfo ? false : this.emit('info', `set Closed Captions: ${state}`);
} catch (error) {
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);
const info = this.disableLogInfo ? false : this.emit('info', `set Media: ${['PLAY', 'PAUSE', 'STOP', 'LOADING', 'INTERRUPTED'][value]}`);
} catch (error) {
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);
const info = this.disableLogInfo ? false : this.emit('info', `set Power Mode Selection: ${command === 'MENU' ? 'SHOW' : 'HIDE'}`);
} catch (error) {
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}`);
const info = this.disableLogInfo ? false : this.emit('info', `set Brightness: ${value}`);
} catch (error) {
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'}`;
const info = this.disableLogInfo ? false : this.emit('info', `set Picture Mode: ${PictureModes[command] ?? 'Unknown'}`);
} catch (error) {
this.emit('warn', `set Picture Mode error: ${error}`);
}
});
}
this.allServices.push(this.televisionService);
//Prepare volume service
if (this.volumeControl > 0) {
const debug3 = this.enableDebugMode ? this.emit('debug', `Prepare television speaker service`) : false;
const volumeServiceName = this.volumeControlNamePrefix ? `${accessoryName} ${this.volumeControlName}` : this.volumeControlName;
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; //none, relative, relative with current, absolute
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);
const info = this.disableLogInfo ? false : this.emit('info', `set Volume Selector: ${command}`);
} catch (error) {
this.emit('warn', `set Volume Selector error: ${error}`);
}
});
this.volumeServiceTvSpeaker.getCharacteristic(Characteristic.Volume)
.onGet(async () => {
const volume = this.volume;
return volume;
})
.onSet(async (volume) => {
try {
const payload = {
volume: volume
};
const cid = await this.lgWebOsSocket.getCid('Audio');
await this.lgWebOsSocket.send('request', ApiUrls.SetVolume, payload, cid);
const info = this.disableLogInfo ? false : this.emit('info', `set Volume: ${volume}`);
} catch (error) {
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);
const info = this.disableLogInfo ? false : this.emit('info', `set Mute: ${state ? 'ON' : 'OFF'}`);
} catch (error) {
this.emit('warn', `set Mute error: ${error}`);
}
});
this.allServices.push(this.volumeServiceTvSpeaker);
//legacy control
switch (this.volumeControl) {
case 1: //lightbulb
const debug = this.enableDebugMode ? this.emit('debug', `Prepare volume service lightbulb`) : false;
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);
});
this.allServices.push(this.volumeServiceLightbulb);
break;
case 2: //fan
const debug1 = this.enableDebugMode ? this.emit('debug', `Prepare volume service fan`) : false;
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)