UNPKG

@su-thomas/homebridge-webos-tv

Version:
1,323 lines (1,033 loc) 92.6 kB
import * as fs from 'fs'; import mkdirp from 'mkdirp'; import LgTvController from './lib/LgTvController.js'; import Events from './lib/Events.js'; let Service, Characteristic, Homebridge, Accessory, HapStatusError, HAPStatus; const PLUGIN_NAME = 'homebridge-webos-tv'; const PLATFORM_NAME = 'webostv'; const PLUGIN_VERSION = '2.4.3'; // General constants const NOT_EXISTING_INPUT = 999999; const DEFAULT_INPUT_SOURCES_LIMIT = 45; const BUTTON_RESET_TIMEOUT = 20; // in milliseconds const AUTOMATIONS_TRIGGER_TIMEOUT = 400; // in milliseconds export default (homebridge) => { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; Homebridge = homebridge; Accessory = homebridge.platformAccessory; HapStatusError = homebridge.hap.HapStatusError; HAPStatus = homebridge.hap.HAPStatus; homebridge.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, webosTvPlatform, true); }; class webosTvDevice { constructor(log, config, api) { this.log = log; this.api = api; // check if we have mandatory device info try { if (!config.ip) throw new Error(`TV ip address is required for ${config.name}`); if (!config.mac) throw new Error(`TV mac address is required for ${config.name}`); } catch (error) { this.logError(error); this.logError(`Failed to create platform device, missing mandatory information!`); this.logError(`Please check your device config!`); return; } // configuration this.name = config.name || 'webOS TV'; this.ip = config.ip; this.mac = config.mac; this.broadcastAdr = config.broadcastAdr || '255.255.255.255'; this.keyFile = config.keyFile; this.prefsDir = config.prefsDir || api.user.storagePath() + '/.webosTv/'; this.alivePollingInterval = config.pollingInterval || 5; this.alivePollingInterval = this.alivePollingInterval * 1000; this.deepDebugLog = config.deepDebugLog; this.silentLog = config.silentLog; if (this.deepDebugLog === undefined) { this.deepDebugLog = false; } if (this.silentLog === undefined) { this.silentLog = false; } this.inputSourcesLimit = config.inputSourcesLimit || DEFAULT_INPUT_SOURCES_LIMIT; this.isHideTvService = config.hideTvService; if (this.isHideTvService === undefined) { this.isHideTvService = false; } this.volumeLimit = config.volumeLimit; if (this.volumeLimit === undefined || isNaN(this.volumeLimit) || this.volumeLimit < 0) { this.volumeLimit = 100; } this.volumeControl = config.volumeControl; if (this.volumeControl === undefined) { this.volumeControl = "both"; } this.channelControl = config.channelControl; if (this.channelControl === undefined) { this.channelControl = true; } this.mediaControl = config.mediaControl; if (this.mediaControl === undefined) { this.mediaControl = false; } this.screenControl = config.screenControl; if (this.screenControl === undefined) { this.screenControl = false; } this.screenSaverControl = config.screenSaverControl; if (this.screenSaverControl === undefined) { this.screenSaverControl = false; } this.serviceMenuButton = config.serviceMenuButton; if (this.serviceMenuButton === undefined) { this.serviceMenuButton = false; } this.ezAdjustButton = config.ezAdjustButton; if (this.ezAdjustButton === undefined) { this.ezAdjustButton = false; } this.backlightControl = config.backlightControl; if (this.backlightControl === undefined) { this.backlightControl = false; } this.brightnessControl = config.brightnessControl; if (this.brightnessControl === undefined) { this.brightnessControl = false; } this.colorControl = config.colorControl; if (this.colorControl === undefined) { this.colorControl = false; } this.contrastControl = config.contrastControl; if (this.contrastControl === undefined) { this.contrastControl = false; } this.ccRemoteRemap = config.ccRemoteRemap; if (this.ccRemoteRemap === undefined) { this.ccRemoteRemap = {}; } this.appButtons = config.appButtons; this.channelButtons = config.channelButtons; this.notificationButtons = config.notificationButtons; this.remoteControlButtons = config.remoteControlButtons; this.soundOutputButtons = config.soundOutputButtons; this.remoteSequenceButtons = config.remoteSequenceButtons; this.pictureModeButtons = config.pictureModeButtons; this.soundModeButtons = config.soundModeButtons; this.systemSettingsButtons = config.systemSettingsButtons; this.triggers = config.triggers; this.logInfo(`Init - got TV configuration, initializing device with name: ${this.name}`); // check if input sources limit is within a reasonable range if (this.inputSourcesLimit < 10) { this.inputSourcesLimit = 10; } if (this.inputSourcesLimit > 65) { this.inputSourcesLimit = 65; } // check if prefs directory ends with a /, if not then add it if (this.prefsDir.endsWith('/') === false) { this.prefsDir = this.prefsDir + '/'; } // check if the tv preferences directory exists, if not then create it if (fs.existsSync(this.prefsDir) === false) { mkdirp(this.prefsDir); } // generate the key file name for the TV if not specified if (this.keyFile === undefined) { this.keyFile = this.prefsDir + 'keyFile_' + this.ip.split('.').join('') + '_' + this.mac.split(':').join(''); } // prepare file paths this.tvInfoFile = this.prefsDir + 'info_' + this.mac.split(':').join(''); this.tvAvailableInputsFile = this.prefsDir + 'inputsAvailable_' + this.mac.split(':').join(''); this.tvInputConfigFile = this.prefsDir + 'inputsConfg_' + this.mac.split(':').join(''); //prepare variables this.dummyInputSourceServices = []; this.configuredInputs = {}; this.tvInputsConfig = {}; // connect to the TV this.connectToTv(); // init the tv accessory this.initTvAccessory(); } /*----------========== SETUP TV DEVICE ==========----------*/ connectToTv() { // create new tv instance and try to connect this.lgTvCtrl = new LgTvController(this.ip, this.mac, this.name, this.keyFile, this.broadcastAdr, this.alivePollingInterval, this.log); this.lgTvCtrl.setVolumeLimit(this.volumeLimit); this.lgTvCtrl.setDeepDebugLogEnabled(this.deepDebugLog); this.lgTvCtrl.setSilentLogEnabled(this.silentLog); this.lgTvCtrl.connect(); //register to listeners this.lgTvCtrl.on(Events.SETUP_FINISHED, () => { this.logInfo('TV setup finished, ready to control tv'); // add external inputs this.initInputSources(); // remove the information service here and add the new one after setup is complete, this way i do not have to save anything? this.updateInformationService(); }); this.lgTvCtrl.on(Events.TV_TURNED_ON, () => { this.updateTvStatusFull(); }); this.lgTvCtrl.on(Events.TV_TURNED_OFF, () => { this.updateTvStatusFull(); }); this.lgTvCtrl.on(Events.PIXEL_REFRESHER_STARTED, () => { this.updateTvStatusFull(); }); this.lgTvCtrl.on(Events.SCREEN_SAVER_TURNED_ON, () => { this.updateScreenSaverStatus(); }); this.lgTvCtrl.on(Events.SCREEN_STATE_CHANGED, () => { this.updateScreenStatus(); }); this.lgTvCtrl.on(Events.POWER_STATE_CHANGED, () => { this.updatePowerStatus(); this.updateScreenStatus(); this.updateScreenSaverStatus(); }); this.lgTvCtrl.on(Events.FOREGROUND_APP_CHANGED, (res) => { this.updateActiveInputSource(); this.updateAppButtons(); this.updateChannelButtons(); }); this.lgTvCtrl.on(Events.AUDIO_STATUS_CHANGED, () => { this.updateTvAudioStatus(); this.updateOccupancyTriggers(); }); this.lgTvCtrl.on(Events.LIVE_TV_CHANNEL_CHANGED, () => { this.updateChannelButtons(); }); this.lgTvCtrl.on(Events.SOUND_OUTPUT_CHANGED, () => { this.updateSoundOutputButtons(); }); this.lgTvCtrl.on(Events.NEW_APP_ADDED, (res) => { if (res) { this.newAppInstalledOnTv(res); } }); this.lgTvCtrl.on(Events.APP_REMOVED, (res) => { if (res) { this.appRemovedFromTv(res.appId); } }); this.lgTvCtrl.on(Events.VOLUME_UP, () => { this.triggerVolumeUpAutomations(); }); this.lgTvCtrl.on(Events.VOLUME_DOWN, (res) => { this.triggerVolumeDownAutomations(); }); this.lgTvCtrl.on(Events.PICTURE_SETTINGS_CHANGED, (res) => { this.updatePictureSettingsServices(); this.updateOccupancyTriggers(); if (this.lgTvCtrl.getCurrentPictureMode()) { this.updatePictureModeButtons(); } }); this.lgTvCtrl.on(Events.SOUND_SETTINGS_CHANGED, (res) => { if (this.lgTvCtrl.getCurrentSoundMode()) { this.updateSoundModeButtons(); } }); Events.SOUND_SETTINGS_CHANGED } /*----------========== SETUP SERVICES ==========----------*/ initTvAccessory() { // generate uuid this.UUID = Homebridge.hap.uuid.generate(this.mac + this.ip); // prepare the tv accessory this.tvAccesory = new Accessory(this.name, this.UUID, Homebridge.hap.Accessory.Categories.TELEVISION); // prepare accessory services this.setupAccessoryServices(); this.api.publishExternalAccessories(PLUGIN_NAME, [this.tvAccesory]); } setupAccessoryServices() { // update the services this.updateInformationService(); // prepare the tv service if (this.isHideTvService === false) { this.prepareTvService(); } // additional services this.prepareVolumeService(); this.prepareChannelControlService(); this.prepareMediaControlService(); this.prepareScreenControlService(); this.prepareScreenSaverControlService(); this.preparServiceMenuButtonService(); this.prepareEzAdjustButtonService(); this.preparePictureSettingsControlServices(); this.prepareAppButtonService(); this.prepareChannelButtonService(); this.prepareNotificationButtonService(); this.prepareRemoteControlButtonService(); this.prepareSoundOutputButtonService(); this.prepareRemoteSequenceButtonsService(); this.preparePictureModeButtonService(); this.prepareSoundModeButtonService(); this.prepareSystemSettingsButtonService(); this.prepareTriggersService(); } // // tv information service ---------------------------------------------------------------- updateInformationService() { let modelName = this.lgTvCtrl.getTvSystemInfo() ? this.lgTvCtrl.getTvSystemInfo().modelName : 'Unknown'; let productName = this.lgTvCtrl.getTvSwInfo() ? `${this.lgTvCtrl.getTvSwInfo().product_name} (${PLUGIN_VERSION})` : PLUGIN_VERSION; let tvFirmwareVer = this.lgTvCtrl.getTvSwInfo() ? this.lgTvCtrl.getTvSwInfo().major_ver + '.' + this.lgTvCtrl.getTvSwInfo().minor_ver : 'Unknown'; // remove the preconstructed information service, since i will be adding my own this.tvAccesory.removeService(this.tvAccesory.getService(Service.AccessoryInformation)); // add my own information service let informationService = new Service.AccessoryInformation(); informationService .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic(Characteristic.Manufacturer, 'LG Electronics') .setCharacteristic(Characteristic.Model, modelName) .setCharacteristic(Characteristic.SerialNumber, productName) .setCharacteristic(Characteristic.FirmwareRevision, tvFirmwareVer); this.tvAccesory.addService(informationService); } // native tv services ---------------------------------------------------------------- prepareTvService() { this.tvService = new Service.Television(this.name, 'tvService'); this.tvService .setCharacteristic(Characteristic.ConfiguredName, this.name); this.tvService .setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE); this.tvService .getCharacteristic(Characteristic.Active) .onGet(this.getPowerState.bind(this)) .onSet(this.setPowerState.bind(this)); this.tvService .setCharacteristic(Characteristic.ActiveIdentifier, NOT_EXISTING_INPUT); // do not preselect any inputs since there are no default inputs this.tvService .getCharacteristic(Characteristic.ActiveIdentifier) .onGet(this.getActiveIdentifier.bind(this)) .onSet(this.setActiveIdentifier.bind(this)); this.tvService .getCharacteristic(Characteristic.RemoteKey) .onSet(this.remoteKeyPress.bind(this)); this.tvService .getCharacteristic(Characteristic.PowerModeSelection) .onSet(this.setPowerModeSelection.bind(this)); // not supported yet?? /* this.tvService .getCharacteristic(Characteristic.PictureMode) .onSet((newValue) => { console.log('set PictureMode => setNewValue: ' + newValue); }); */ this.tvAccesory.addService(this.tvService); // prepare the additional native services - control center tv speaker and inputs this.prepareTvSpeakerService(); this.prepareInputSourcesService(); } prepareTvSpeakerService() { const serviceName = this.name + ' Volume'; this.tvSpeakerService = new Service.TelevisionSpeaker(serviceName, 'tvSpeakerService'); this.tvSpeakerService .setCharacteristic(Characteristic.ConfiguredName, serviceName); this.tvSpeakerService .setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE) .setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); this.tvSpeakerService .getCharacteristic(Characteristic.VolumeSelector) .onSet(this.setVolumeSelectorState.bind(this)); this.tvSpeakerService .getCharacteristic(Characteristic.Mute) .onGet(this.getMuteState.bind(this)) .onSet(this.setMuteState.bind(this)); this.tvSpeakerService .addCharacteristic(Characteristic.Volume) .onGet(this.getVolume.bind(this)) .onSet(this.setVolume.bind(this)); this.tvService.addLinkedService(this.tvSpeakerService); this.tvAccesory.addService(this.tvSpeakerService); } prepareInputSourcesService() { // create dummy inputs for (var i = 0; i < this.inputSourcesLimit; i++) { let inputId = i; let dummyInputSource = new Service.InputSource('dummy', `input_${inputId}`); dummyInputSource .setCharacteristic(Characteristic.Identifier, inputId) .setCharacteristic(Characteristic.ConfiguredName, 'dummy') .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.NOT_CONFIGURED) .setCharacteristic(Characteristic.TargetVisibilityState, Characteristic.TargetVisibilityState.HIDDEN) .setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.HIDDEN); // add the new dummy input source service to the tv accessory this.tvService.addLinkedService(dummyInputSource); this.tvAccesory.addService(dummyInputSource); // keep references to all free dummy input services this.dummyInputSourceServices.push(dummyInputSource); } // read out the saved tv inputs let availableInputs = []; try { availableInputs = JSON.parse(fs.readFileSync(this.tvAvailableInputsFile)); } catch (err) { this.logDebug('The TV has no configured inputs yet!'); } // read out the tv input sources config try { this.tvInputsConfig = JSON.parse(fs.readFileSync(this.tvInputConfigFile)); } catch (err) { this.logDebug('No TV inputs config file found!'); } // add the saved inputs //Note to myself, i am saving the inputs in a file as a cache in order when the user starts homebridge and the tv is off that the cached inputs got added already. this.addInputSources(availableInputs); } addInputSources(inputSourcesList) { // if the tv service is hidden then we cannot add any input sources so just skip if (this.isHideTvService) { return; } // make sure we always have an array here if (!inputSourcesList || Array.isArray(inputSourcesList) === false) { inputSourcesList = []; } this.logDebug(`Adding ${inputSourcesList.length} new input sources!`); for (let value of inputSourcesList) { if (this.dummyInputSourceServices.length === 0) { this.logWarn(`Inputs limit (${this.inputSourcesLimit}) reached. Cannot add any more new inputs!`); break; } var inputSourceService = this.dummyInputSourceServices.shift(); // get the first free input source service // create a new input definition let newInputDef = {}; // get appId newInputDef.appId = value.appId; // if appId null or empty then skip this input, appId is required to open an app if (!newInputDef.appId || newInputDef.appId === '' || typeof newInputDef.appId !== 'string') { this.logWarn(`Missing appId or appId is not of type string. Cannot add input source!`); return; } // remove all white spaces from the appId string newInputDef.appId = newInputDef.appId.replace(/\s/g, ''); //appId newInputDef.appId = newInputDef.appId; // name (name - input config, label - auto generated inputs) newInputDef.name = value.name || value.label || newInputDef.appId; // if we have a saved name in the input sources config then use that if (this.tvInputsConfig[newInputDef.appId] && this.tvInputsConfig[newInputDef.appId].name) { newInputDef.name = this.tvInputsConfig[newInputDef.appId].name; } // params newInputDef.params = value.params || {}; //input Identifier newInputDef.id = inputSourceService.getCharacteristic(Characteristic.Identifier).value; let visible = false; if (this.tvInputsConfig[newInputDef.appId] && this.tvInputsConfig[newInputDef.appId].visible === true) { visible = true; } inputSourceService .setCharacteristic(Characteristic.Name, newInputDef.name) .setCharacteristic(Characteristic.ConfiguredName, newInputDef.name) .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) .setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.APPLICATION) .setCharacteristic(Characteristic.TargetVisibilityState, visible ? Characteristic.TargetVisibilityState.SHOWN : Characteristic.TargetVisibilityState.HIDDEN) .setCharacteristic(Characteristic.CurrentVisibilityState, visible ? Characteristic.CurrentVisibilityState.SHOWN : Characteristic.CurrentVisibilityState.HIDDEN); // set visibility state inputSourceService.getCharacteristic(Characteristic.TargetVisibilityState) .onSet((state) => { this.setInputTargetVisibility(state, newInputDef); }); // set input name inputSourceService.getCharacteristic(Characteristic.ConfiguredName) .onSet((value) => { this.setInputConfiguredName(value, newInputDef); }); // add a reference to the input source to the new input and add it to the configured inputs list newInputDef.inputService = inputSourceService; this.configuredInputs[newInputDef.id] = newInputDef; this.logDebug(`Created new input source: appId: ${newInputDef.appId}, name: ${newInputDef.name}`); } } removeInputSource(inputDef) { // removed it from configured inptuts delete this.configuredInputs[inputDef.id]; // reset dummy info let inputService = inputDef.inputService; inputService .setCharacteristic(Characteristic.Name, 'dummy') .setCharacteristic(Characteristic.ConfiguredName, 'dummy') .setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.NOT_CONFIGURED) .setCharacteristic(Characteristic.TargetVisibilityState, Characteristic.TargetVisibilityState.HIDDEN) .setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.HIDDEN); // readd to the dummy list as free this.dummyInputSourceServices.push(inputService); } // additional services ---------------------------------------------------------------- prepareVolumeService() { if (!this.volumeControl || this.volumeControl === "none") { return; } // slider - lightbulb or fan if (this.volumeControl === true || this.volumeControl === "both" || this.volumeControl === 'slider' || this.volumeControl === 'lightbulb') { this.volumeAsLightbulbService = new Service.Lightbulb('Volume', 'volumeService'); this.volumeAsLightbulbService.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeAsLightbulbService.setCharacteristic(Characteristic.ConfiguredName, 'Volume'); this.volumeAsLightbulbService .getCharacteristic(Characteristic.On) .onGet(this.getLightbulbMuteState.bind(this)) .onSet(this.setLightbulbMuteState.bind(this)); this.volumeAsLightbulbService .addCharacteristic(new Characteristic.Brightness()) .onGet(this.getLightbulbVolume.bind(this)) .onSet(this.setLightbulbVolume.bind(this)); this.tvAccesory.addService(this.volumeAsLightbulbService); } else if (this.volumeControl === "fan") { this.volumeAsFanService = new Service.Fanv2('Volume', 'volumeService'); this.volumeAsFanService.addOptionalCharacteristic(Characteristic.ConfiguredName); this.volumeAsFanService.setCharacteristic(Characteristic.ConfiguredName, 'Volume'); this.volumeAsFanService .getCharacteristic(Characteristic.Active) .onGet(this.getFanMuteState.bind(this)) .onSet(this.setFanMuteState.bind(this)); this.volumeAsFanService.addCharacteristic(Characteristic.RotationSpeed) .onGet(this.getRotationSpeedVolume.bind(this)) .onSet(this.setRotationSpeedVolume.bind(this)); this.tvAccesory.addService(this.volumeAsFanService); } // volume up/down buttons if (this.volumeControl === true || this.volumeControl === "both" || this.volumeControl === 'buttons') { this.volumeUpService = this.createStatlessSwitchService('Volume Up', 'volumeUpService', this.setVolumeUp.bind(this)); this.tvAccesory.addService(this.volumeUpService); this.volumeDownService = this.createStatlessSwitchService('Volume Down', 'volumeDownService', this.setVolumeDown.bind(this)); this.tvAccesory.addService(this.volumeDownService); } } prepareChannelControlService() { if (!this.channelControl) { return; } this.channelUpService = this.createStatlessSwitchService('Channel Up', 'channelUpService', this.setChannelUp.bind(this)); this.tvAccesory.addService(this.channelUpService); this.channelDownService = this.createStatlessSwitchService('Channel Down', 'channelDownService', this.setChannelDown.bind(this)); this.tvAccesory.addService(this.channelDownService); } prepareMediaControlService() { if (!this.mediaControl) { return; } this.mediaPlayService = this.createStatlessSwitchService('Play', 'mediaPlayService', this.setPlay.bind(this)); this.tvAccesory.addService(this.mediaPlayService); this.mediaPauseService = this.createStatlessSwitchService('Pause', 'mediaPauseService', this.setPause.bind(this)); this.tvAccesory.addService(this.mediaPauseService); this.mediaStopService = this.createStatlessSwitchService('Stop', 'mediaStopService', this.setStop.bind(this)); this.tvAccesory.addService(this.mediaStopService); this.mediaRewindService = this.createStatlessSwitchService('Rewind', 'mediaRewindService', this.setRewind.bind(this)); this.tvAccesory.addService(this.mediaRewindService); this.mediaFastForwardService = this.createStatlessSwitchService('Fast Forward', 'mediaFastForwardService', this.setFastForward.bind(this)); this.tvAccesory.addService(this.mediaFastForwardService); } prepareScreenControlService() { if (!this.screenControl) { return; } // create the service this.screenControlService = new Service.Switch('Screen', 'screenControlService'); this.screenControlService.addOptionalCharacteristic(Characteristic.ConfiguredName); this.screenControlService.setCharacteristic(Characteristic.ConfiguredName, 'Screen'); this.screenControlService .getCharacteristic(Characteristic.On) .onGet(this.getTvScreenState.bind(this)) .onSet(this.setTvScreenState.bind(this)); this.tvAccesory.addService(this.screenControlService); } prepareScreenSaverControlService() { if (!this.screenSaverControl) { return; } // create the service this.screenSaverControlService = new Service.Switch('Screen Saver', 'screenSaverControlService'); this.screenSaverControlService.addOptionalCharacteristic(Characteristic.ConfiguredName); this.screenSaverControlService.setCharacteristic(Characteristic.ConfiguredName, 'Screen Saver'); this.screenSaverControlService .getCharacteristic(Characteristic.On) .onGet(this.getScreenSaverState.bind(this)) .onSet(this.setScreenSaverState.bind(this)); this.tvAccesory.addService(this.screenSaverControlService); } preparServiceMenuButtonService() { if (!this.serviceMenuButton) { return; } this.serviceMenuButtonService = this.createStatlessSwitchService('Service Menu', 'serviceMenuButtonService', this.setServiceMenu.bind(this)); this.tvAccesory.addService(this.serviceMenuButtonService); } prepareEzAdjustButtonService() { if (!this.ezAdjustButton) { return; } this.ezAdjustButtonService = this.createStatlessSwitchService('ezAdjust', 'ezAdjustButtonService', this.setEzAdjust.bind(this)); this.tvAccesory.addService(this.ezAdjustButtonService); } preparePictureSettingsControlServices() { if (this.backlightControl) { this.backlightControlService = this.createPictureSettingsLightbulbService('Backlight', 'backlightControlService', this.setLightbulbBacklightOnState, this.setLightbulbBacklight, this.getLightbulbBacklight, ); this.tvAccesory.addService(this.backlightControlService); } if (this.brightnessControl) { this.brightnessControlService = this.createPictureSettingsLightbulbService('Brightness', 'brightnessControlService', this.setLightbulbBrightnessOnState, this.setLightbulbBrightness, this.getLightbulbBrightness, ); this.tvAccesory.addService(this.brightnessControlService); } if (this.colorControl) { this.colorControlService = this.createPictureSettingsLightbulbService('Color', 'colorControlService', this.setLightbulbColorOnState, this.setLightbulbColor, this.getLightbulbColor, ); this.tvAccesory.addService(this.colorControlService); } if (this.contrastControl) { this.contrastControlService = this.createPictureSettingsLightbulbService('Contrast', 'contrastControlService', this.setLightbulbContrastOnState, this.setLightbulbContrast, this.getLightbulbContrast, ); this.tvAccesory.addService(this.contrastControlService); } } prepareAppButtonService() { if (this.checkArrayConfigProperty(this.appButtons, "appButtons") === false) { return; } this.configuredAppButtons = {}; this.appButtons.forEach((value, i) => { // create a new app button definition let newAppButtonDef = {}; // get appid newAppButtonDef.appId = value.appId || value; // if appId null or empty then skip this app button, appId is required to open an app if (!newAppButtonDef.appId || newAppButtonDef.appId === '' || typeof newAppButtonDef.appId !== 'string') { this.logWarn(`Missing appId or appId in not of type string. Cannot add app button!`); return; } // remove all white spaces from the appId string newAppButtonDef.appId = newAppButtonDef.appId.replace(/\s/g, ''); // get name newAppButtonDef.name = value.name || 'App - ' + newAppButtonDef.appId; // params newAppButtonDef.params = value.params || {}; // create the service let newAppButtonService = this.createStatefulSwitchService(newAppButtonDef.name, 'appButtonService' + i, () => { return this.getAppButtonState(newAppButtonDef.appId); }, (state) => { this.setAppButtonState(state, newAppButtonDef); }); // add to the tv service this.tvAccesory.addService(newAppButtonService); // save the configured channel button service newAppButtonDef.switchService = newAppButtonService; this.configuredAppButtons[newAppButtonDef.appId + i] = newAppButtonDef; // need to add i here to the appid since a user can configure multiple appbuttons with the same appid }); } prepareChannelButtonService() { if (this.checkArrayConfigProperty(this.channelButtons, "channelButtons") === false) { return; } this.configuredChannelButtons = {}; this.channelButtons.forEach((value, i) => { // create a new channel button definition let newChannelButtonDef = {}; // get the channelNumber newChannelButtonDef.channelNumber = value.channelNumber || value; // if channelNumber null or is not a number then skip this channel button, channelNumber is required if (Number.isInteger(parseInt(newChannelButtonDef.channelNumber)) === false) { this.logWarn(`Missing channelNumber or channelNumber is not a number. Cannot add channel button!`); return; } // convert to string if the channel number was not a string newChannelButtonDef.channelNumber = newChannelButtonDef.channelNumber.toString(); // get channelId newChannelButtonDef.channelId = value.channelId; // get name newChannelButtonDef.name = value.name || 'Channel - ' + newChannelButtonDef.channelNumber; // create the service let newChannelButtonService = this.createStatefulSwitchService(newChannelButtonDef.name, 'channelButtonService' + i, () => { return this.getChannelButtonState(newChannelButtonDef.channelNumber); }, (state) => { this.setChannelButtonState(state, newChannelButtonDef); }); // add to the tv service this.tvAccesory.addService(newChannelButtonService); // save the configured channel button service newChannelButtonDef.switchService = newChannelButtonService; this.configuredChannelButtons[newChannelButtonDef.channelNumber] = newChannelButtonDef; }); } prepareNotificationButtonService() { if (this.checkArrayConfigProperty(this.notificationButtons, "notificationButtons") === false) { return; } this.configuredNotificationButtons = []; this.notificationButtons.forEach((value, i) => { // create a new notification button definition let newNotificationButtonDef = {}; // get the notification message newNotificationButtonDef.message = value.message || value; // if message null or empty then skip this notification button, message is required to display a notification if (!newNotificationButtonDef.message || typeof newNotificationButtonDef.message !== 'string' || newNotificationButtonDef.message === '') { this.logWarn(`Missing message or message is not of type string. Cannot add notification button!`); return; } // get name newNotificationButtonDef.name = value.name || 'Notification - ' + newNotificationButtonDef.message; // get the appId if specified newNotificationButtonDef.appId = value.appId; // params newNotificationButtonDef.params = value.params || {}; // get the optional notification content file, if that is specified then the content of this file is read and displayed in the notification if (value.file && typeof value.file === 'string' && value.file.length > 0) { newNotificationButtonDef.file = value.file; // if only file name specified then look for the file in the prefsdir if (newNotificationButtonDef.file.includes('/') === false) { newNotificationButtonDef.file = this.prefsDir + newNotificationButtonDef.file; } } // create the stateless button service let newNotificationButtonService = this.createStatlessSwitchService(newNotificationButtonDef.name, 'notificationButtonService' + i, (state) => { this.setNotificationButtonState(state, newNotificationButtonDef); }); this.tvAccesory.addService(newNotificationButtonService); // save the configured notification button service newNotificationButtonDef.switchService = newNotificationButtonService; this.configuredNotificationButtons.push(newNotificationButtonDef); }); } prepareRemoteControlButtonService() { if (this.checkArrayConfigProperty(this.remoteControlButtons, "remoteControlButtons") === false) { return; } this.configuredRemoteControlButtons = []; this.remoteControlButtons.forEach((value, i) => { // create a new remote control button definition let newRemoteControlButtonDef = {}; // get the remote control action newRemoteControlButtonDef.action = value.action || value; // if action null or empty then skip this remote control button, action is required for a remote control button if (!newRemoteControlButtonDef.action || newRemoteControlButtonDef.action === '' || typeof newRemoteControlButtonDef.action !== 'string') { this.logWarn(`Missing action or action is not of type string. Cannot add remote control button!`); return; } // make sure the action is string and uppercase newRemoteControlButtonDef.action = newRemoteControlButtonDef.action.toString().toUpperCase(); // get name newRemoteControlButtonDef.name = value.name || 'Remote - ' + newRemoteControlButtonDef.action; // create the stateless button service let newRemoteControlButtonService = this.createStatlessSwitchService(newRemoteControlButtonDef.name, 'remoteControlButtonService' + i, (state) => { this.setRemoteControlButtonState(state, newRemoteControlButtonDef.action); }); this.tvAccesory.addService(newRemoteControlButtonService); // save the configured remote control button service newRemoteControlButtonDef.switchService = newRemoteControlButtonService; this.configuredRemoteControlButtons.push(newRemoteControlButtonDef); }); } prepareSoundOutputButtonService() { if (this.checkArrayConfigProperty(this.soundOutputButtons, "soundOutputButtons") === false) { return; } this.configuredSoundOutputButtons = {}; this.soundOutputButtons.forEach((value, i) => { // create a new sound output button definition let newSoundOutputButtonDef = {}; // get the sound output id newSoundOutputButtonDef.soundOutput = value.soundOutput || value; // if soundOutput null or empty then skip this sound output button, soundOutput is required for a sound output button if (!newSoundOutputButtonDef.soundOutput || newSoundOutputButtonDef.soundOutput === '' || typeof newSoundOutputButtonDef.soundOutput !== 'string') { this.logWarn(`Missing soundOutput or soundOutput is not of type string. Cannot add sound output button!`); return; } // make sure the soundOutput is string newSoundOutputButtonDef.soundOutput = newSoundOutputButtonDef.soundOutput.toString(); // get name newSoundOutputButtonDef.name = value.name || 'Sound Output - ' + newSoundOutputButtonDef.soundOutput; // create the service let newSoundOutputButtonService = this.createStatefulSwitchService(newSoundOutputButtonDef.name, 'soundOutputButtonService' + i, () => { return this.getSoundOutputButtonState(newSoundOutputButtonDef.soundOutput); }, (state) => { this.setSoundOutputButtonState(state, newSoundOutputButtonDef.soundOutput); }); // add to the tv service this.tvAccesory.addService(newSoundOutputButtonService); // save the configured sound output button service newSoundOutputButtonDef.switchService = newSoundOutputButtonService; this.configuredSoundOutputButtons[newSoundOutputButtonDef.soundOutput] = newSoundOutputButtonDef; }); } preparePictureModeButtonService() { if (this.checkArrayConfigProperty(this.pictureModeButtons, "pictureModeButtons") === false) { return; } this.configuredPictureModeButtons = []; this.pictureModeButtons.forEach((value, i) => { // create a new picture mode button definition let newPictureModeButtonDef = {}; // get the picture mode name newPictureModeButtonDef.pictureMode = value.pictureMode || value; // if pictureMode null or empty then skip this picture mode button, pictureMode is required for a picture mode button if (!newPictureModeButtonDef.pictureMode || newPictureModeButtonDef.pictureMode === '' || typeof newPictureModeButtonDef.pictureMode !== 'string') { this.logWarn(`Missing pictureMode or pictureMode is not of type string. Cannot add picture mode button!`); return; } // make sure the pictureMode is string newPictureModeButtonDef.pictureMode = newPictureModeButtonDef.pictureMode.toString(); // get name newPictureModeButtonDef.name = value.name || 'Picture Mode - ' + newPictureModeButtonDef.pictureMode; // create the service let newPictureModeButtonService = this.createStatefulSwitchService(newPictureModeButtonDef.name, 'pictureModeButtonsService' + i, () => { return this.getPictureModeButtonState(newPictureModeButtonDef.pictureMode); }, (state) => { this.setPictureModeButtonState(state, newPictureModeButtonDef.pictureMode, true); }); this.tvAccesory.addService(newPictureModeButtonService); // save the configured picture mode button service newPictureModeButtonDef.switchService = newPictureModeButtonService; this.configuredPictureModeButtons.push(newPictureModeButtonDef); }); } prepareSoundModeButtonService() { if (this.checkArrayConfigProperty(this.soundModeButtons, "soundModeButtons") === false) { return; } this.configuredSoundModeButtons = []; this.soundModeButtons.forEach((value, i) => { // create a new sound mode button definition let newSoundModeButtonDef = {}; // get the sound mode name newSoundModeButtonDef.soundMode = value.soundMode || value; // if soundMode null or empty then skip this sound mode button, soundMode is required for a sound mode button if (!newSoundModeButtonDef.soundMode || newSoundModeButtonDef.soundMode === '' || typeof newSoundModeButtonDef.soundMode !== 'string') { this.logWarn(`Missing soundMode or soundMode is not of type string. Cannot add sound mode button!`); return; } // make sure the soundMode is string newSoundModeButtonDef.soundMode = newSoundModeButtonDef.soundMode.toString(); // get name newSoundModeButtonDef.name = value.name || 'Sound Mode - ' + newSoundModeButtonDef.soundMode; // create the service let newSoundModeButtonService = this.createStatefulSwitchService(newSoundModeButtonDef.name, 'soundModeButtonsService' + i, () => { return this.getSoundModeButtonState(newSoundModeButtonDef.soundMode); }, (state) => { this.setSoundModeButtonState(state, newSoundModeButtonDef.soundMode); }); // add to the tv service this.tvAccesory.addService(newSoundModeButtonService); // save the configured sound mode button service newSoundModeButtonDef.switchService = newSoundModeButtonService; this.configuredSoundModeButtons[newSoundModeButtonDef.soundMode] = newSoundModeButtonDef; }); } prepareSystemSettingsButtonService() { if (this.checkArrayConfigProperty(this.systemSettingsButtons, "systemSettingsButtons") === false) { return; } this.configuredSystemSettingsButtons = []; this.systemSettingsButtons.forEach((value, i) => { // create a new system setting button definition let newSystemSettingsButtonDef = {}; // get the category object newSystemSettingsButtonDef.category = value.category; // if category null or empty then skip this system settings button, category is required for a system settings button if (!newSystemSettingsButtonDef.category || newSystemSettingsButtonDef.category === '' || typeof newSystemSettingsButtonDef.category !== 'string') { this.logWarn(`Missing category or category is not of type string. Cannot add system settings button!`); return; } // get the settings object newSystemSettingsButtonDef.settings = value.settings; // if settings null or empty then skip this system settings button, settings is required for a system settings button if (newSystemSettingsButtonDef.settings === null || newSystemSettingsButtonDef.settings === undefined) { this.logWarn(`Missing settings defintion. Cannot add system settings button!`); return; } // get name newSystemSettingsButtonDef.name = value.name || 'System Settings - ' + i; // create the stateless button service let newSystemModeSettingsService = this.createStatlessSwitchService(newSystemSettingsButtonDef.name, 'systemSettingsService' + i, (state) => { this.setSystemSettingsButtonState(state, newSystemSettingsButtonDef); }); this.tvAccesory.addService(newSystemModeSettingsService); // save the configured system settings button service newSystemSettingsButtonDef.switchService = newSystemModeSettingsService; this.configuredSystemSettingsButtons.push(newSystemSettingsButtonDef); }); } prepareRemoteSequenceButtonsService() { if (this.checkArrayConfigProperty(this.remoteSequenceButtons, "remoteSequenceButtons") === false) { return; } this.configuredRemoteSequenceButtons = []; this.remoteSequenceButtons.forEach((value, i) => { // create a new remote sequence button definition let newRemoteSequenceButtonDef = {}; // get the sequence newRemoteSequenceButtonDef.sequence = value.sequence || value; // check if everything is fine if (newRemoteSequenceButtonDef.sequence === null || newRemoteSequenceButtonDef.sequence === undefined || Array.isArray(newRemoteSequenceButtonDef.sequence) === false) { this.logWarn(`Missing sequence defintion. Cannot add remote sequence button!`); return; } // get sequence name newRemoteSequenceButtonDef.name = value.name || 'Sequence ' + i; // get/adjust sequence interval newRemoteSequenceButtonDef.interval = [500]; // default value if (value.interval) { if (Array.isArray(value.interval) === false) { if (isNaN(value.interval) === false) { //single value newRemoteSequenceButtonDef.interval = [parseInt(value.interval)]; } } else { // list of intervals newRemoteSequenceButtonDef.interval = value.interval; } } // create the stateless button service let newRemoteSequenceButtonService = this.createStatlessSwitchService(newRemoteSequenceButtonDef.name, 'remoteSequenceButtonsService' + i, (state) => { this.setRemoteSequenceButtonState(state, newRemoteSequenceButtonDef); }); this.tvAccesory.addService(newRemoteSequenceButtonService); // save the configured remote sequence button service newRemoteSequenceButtonDef.switchService = newRemoteSequenceButtonService; this.configuredRemoteSequenceButtons.push(newRemoteSequenceButtonDef); }); } prepareTriggersService() { if (!this.triggers) { return; } if (this.triggers.volume) { this.volumeTriggerDef = this.createTriggerOccupancySensor('volume', this.getTvVolume.bind(this)); this.tvAccesory.addService(this.volumeTriggerDef.service); } if (this.triggers.backlight) { this.backlightTriggerDef = this.createTriggerOccupancySensor('backlight', this.getLightbulbBacklight.bind(this)); this.tvAccesory.addService(this.backlightTriggerDef.service); } if (this.triggers.brightness) { this.brightnessTriggerDef = this.createTriggerOccupancySensor('brightness', this.getLightbulbBrightness.bind(this)); this.tvAccesory.addService(this.brightnessTriggerDef.service); } if (this.triggers.color) { this.colorTriggerDef = this.createTriggerOccupancySensor('color', this.getLightbulbColor.bind(this)); this.tvAccesory.addService(this.colorTriggerDef.service); } if (this.triggers.contrast) { this.contrastTriggerDef = this.createTriggerOccupancySensor('contrast', this.getLightbulbContrast.bind(this)); this.tvAccesory.addService(this.contrastTriggerDef.service); } } /*----------========== HOMEBRIDGE STATE SETTERS/GETTERS ==========----------*/ /*---=== Tv service ===---*/ // Power getPowerState() { return this.isTvOn() ? Characteristic.Active.ACTIVE : Characteristic.Active.INACTIVE; } setPowerState(state) { if (this.lgTvCtrl) { let isPowerOn = state === Characteristic.Active.ACTIVE; this.lgTvCtrl.setTvPowerState(isPowerOn); } else { throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); } } // Active identifier getActiveIdentifier() { return this.getActiveInputId(); } setActiveIdentifier(inputIdentifier) { this.logDebug('Trying to switch input to identifier: %d', inputIdentifier); if (this.configuredInputs[inputIdentifier]) { this.logDebug('Input source changed, new input source identifier: %d, source appId: %s', inputIdentifier, this.configuredInputs[inputIdentifier].appId); this.lgTvCtrl.turnOnTvAndLaunchApp(this.configuredInputs[inputIdentifier].appId, this.configuredInputs[inputIdentifier].params); } else { this.logDebug('No configured input with identifier: %d found!', inputIdentifier); } } // Volume selector setVolumeSelectorState(state) { this.logDebug('Volume change over the remote control (VolumeSelector), pressed: %s', state === Characteristic.VolumeSelector.DECREMENT ? 'Down' : 'Up'); if (state === Characteristic.VolumeSelector.DECREMENT) { this.tvVolumeDown(); } else { this.tvVolumeUp(); } } // Mute getMuteState() { return this.isTvMuted(); } setMuteState(state) { if (this.isTvOn()) { this.lgTvCtrl.setMute(state); } } // volume level getVolume() { return this.getTvVolume(); } setVolume(level) { if (this.isTvOn()) { this.lgTvCtrl.setVolumeLevel(level); } } // cc remote control remoteKeyPress(remoteKey) { switch (remoteKey) { case Characteristic.RemoteKey.REWIND: this.lgTvCtrl.sendRemoteInputSocketCommand('REWIND'); break; case Characteristic.RemoteKey.FAST_FORWARD: this.lgTvCtrl.sendRemoteInputSocketCommand('FASTFORWARD'); break; case Characteristic.RemoteKey.NEXT_TRACK: this.logDebug('Next track remote key not supported'); break; case Characteristic.RemoteKey.PREVIOUS_TRACK: this.logDebug('Previous track remote key not supported'); break; case Characteristic.RemoteKey.ARROW_UP: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('arrowup', 'UP')); break; case Characteristic.RemoteKey.ARROW_DOWN: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('arrowdown', 'DOWN')); break; case Characteristic.RemoteKey.ARROW_LEFT: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('arrowleft', 'LEFT')); break; case Characteristic.RemoteKey.ARROW_RIGHT: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('arrowright', 'RIGHT')); break; case Characteristic.RemoteKey.SELECT: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('select', 'ENTER')); break; case Characteristic.RemoteKey.BACK: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('back', 'BACK')); break; case Characteristic.RemoteKey.EXIT: this.lgTvCtrl.sendRemoteInputSocketCommand('EXIT'); break; case Characteristic.RemoteKey.PLAY_PAUSE: if (this.getCcRemapCmd('playpause')) { this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('playpause')); } else { this.lgTvCtrl.sendPlayPause(); } break; case Characteristic.RemoteKey.INFORMATION: this.lgTvCtrl.sendRemoteInputSocketCommand(this.getCcRemapCmd('information', 'INFO')); break; } } //power mode selection setPowerModeSelection(newValue) { this.logDebug('Requested tv settings (PowerModeSelection): ' + newValue); this.lgTvCtrl.sendRemoteInputSocketCommand('MENU'); } //inputs config setInputTargetVisibility(state, inputDef) { if (this.configuredInputs[inputDef.id]) { let isVisible = state === Characteristic.TargetVisibilityState.SHOWN ? true : false; this.logDebug(`Setting ${isVisible ? 'VISIBLE' : 'HIDDEN' } for input with name: ${inputDef.name} id: ${inputDef.id}`); let newVisibilityState = isVisible ? Characteristic.CurrentVisibilityState.SHOWN : Characteristic.CurrentVisibil