UNPKG

homebridge-lgwebos-tv

Version:
958 lines (882 loc) • 118 kB
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)