UNPKG

homebridge-lgwebos-tv

Version:
870 lines (802 loc) • 101 kB
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