using LgTv; using Microsoft.Win32; using Newtonsoft.Json; using NWin32; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; using System.Windows.Forms; namespace ColorControl { class LgService : ServiceBase { internal enum PowerOnOffState { StartUp = 1, Resume = 2, ScreenSaver = 3, ShutDown = 4, StandBy = 5 } internal class ProcessMonitorContext { public List Devices { get; set; } public bool WasFullScreen { get; set; } public Process[] StartedProcesses { get; set; } public Process[] StoppedProcesses { get; set; } public Process[] RunningProcesses { get; set; } public bool IsNotificationDisabled { get; set; } public Process ForegroundProcess { get; set; } public bool ForegroundProcessIsFullScreen { get; set; } public string LastFullScreenProcessName = string.Empty; } protected static readonly NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); public static string LgListAppsJson = "listApps.json"; public List Devices { get; private set; } public LgDevice SelectedDevice { get { return _selectedDevice; } set { _selectedDevice = value; Config.PreferredMacAddress = _selectedDevice != null ? _selectedDevice.MacAddress : null; } } public LgServiceConfig Config { get; private set; } private string _configFilename; private bool _allowPowerOn; private LgDevice _selectedDevice; private string _rcButtonsFilename; private List _remoteControlButtons; private List _lgApps = new List(); private bool _poweredOffByScreenSaver; private int _poweredOffByScreenSaverProcessId; private Task _monitorTask; private int _monitorTaskCounter; private LgPreset _lastTriggeredPreset; public LgService(string dataPath, bool allowPowerOn) : base(dataPath, "LgPresets.json") { _allowPowerOn = allowPowerOn; LgPreset.LgApps = _lgApps; LoadConfig(); LoadPresets(); LoadRemoteControlButtons(); } ~LgService() { GlobalSave(); } public static async Task ExecutePresetAsync(string presetName) { try { var lgService = new LgService(Program.DataDir, true); await lgService.RefreshDevices(afterStartUp: true); var result = await lgService.ApplyPreset(presetName); if (!result) { Console.WriteLine("Preset not found or error while executing."); } return result; } catch (Exception ex) { Console.WriteLine("Error executing preset: " + ex.ToLogString()); return false; } } public void GlobalSave() { SaveConfig(); SavePresets(); SaveRemoteControlButtons(); } public LgPreset CreateNewPreset() { var preset = new LgPreset(); var name = "New preset"; string fullname; var number = 1; do { fullname = $"{name} ({number})"; number++; } while (_presets.Any(x => x.name.Equals(fullname))); preset.name = fullname; return preset; } protected override List GetDefaultPresets() { List presets = null; var defaultPresetsFileName = Path.Combine(Directory.GetCurrentDirectory(), "LgPresets.json"); if (File.Exists(defaultPresetsFileName)) { var json = File.ReadAllText(defaultPresetsFileName); presets = _JsonDeserializer.Deserialize>(json); } return presets; } private void LoadConfig() { _configFilename = Path.Combine(_dataPath, "LgConfig.json"); if (File.Exists(_configFilename)) { Config = JsonConvert.DeserializeObject(File.ReadAllText(_configFilename)); } else { Config = new LgServiceConfig(); } LgPreset.LgDevices = Config.Devices; } private void LoadRemoteControlButtons() { var defaultButtons = GenerateDefaultRemoteControlButtons(); _rcButtonsFilename = Path.Combine(_dataPath, "LgRemoteControlButtons.json"); //if (File.Exists(_rcButtonsFilename)) //{ // var json = File.ReadAllText(_rcButtonsFilename); // _remoteControlButtons = _JsonDeserializer.Deserialize>(json); // var missingButtons = defaultButtons.Where(b => !_remoteControlButtons.Any(x => x.name.Equals(b.name))); // _remoteControlButtons.AddRange(missingButtons); //} //else { _remoteControlButtons = defaultButtons; } } public List GetRemoteControlButtons() { return _remoteControlButtons; } private List GenerateDefaultRemoteControlButtons() { var list = new List(); for (var i = 1; i < 11; i++) { var button = i < 10 ? i : 0; list.Add(GeneratePreset(button.ToString())); } list.Add(GeneratePreset("Power", step: "POWER")); list.Add(GeneratePreset("Home", appId: "com.webos.app.home", key: Keys.H)); list.Add(GeneratePreset("Settings", appId: "com.palm.app.settings", key: Keys.S)); list.Add(GeneratePreset("TV Guide", appId: "com.webos.app.livemenu")); list.Add(GeneratePreset("List", step: "LIST")); list.Add(GeneratePreset("SAP", step: "SAP")); list.Add(GeneratePreset("Vol +", step: "VOLUMEUP", key: Keys.VolumeUp)); list.Add(GeneratePreset("Vol -", step: "VOLUMEDOWN", key: Keys.VolumeDown)); list.Add(GeneratePreset("Mute", step: "MUTE", key: Keys.VolumeMute)); list.Add(GeneratePreset("Channel +", step: "CHANNELUP", key: Keys.MediaNextTrack)); list.Add(GeneratePreset("Channel -", step: "CHANNELDOWN", key: Keys.MediaPreviousTrack)); list.Add(GeneratePreset("Up", step: "UP", key: Keys.Up)); list.Add(GeneratePreset("Down", step: "DOWN", key: Keys.Down)); list.Add(GeneratePreset("Left", step: "LEFT", key: Keys.Left)); list.Add(GeneratePreset("Right", step: "RIGHT", key: Keys.Right)); list.Add(GeneratePreset("Enter", step: "ENTER", key: Keys.Enter)); list.Add(GeneratePreset("Back", step: "BACK", key: Keys.Back)); list.Add(GeneratePreset("Exit", step: "EXIT", key: Keys.Escape)); list.Add(GeneratePreset("Netflix", appId: "netflix")); list.Add(GeneratePreset("Inputs", appId: "com.webos.app.homeconnect")); list.Add(GeneratePreset("Amazon Prime", appId: "amazon")); list.Add(GeneratePreset("Red", step: "RED")); list.Add(GeneratePreset("Green", step: "GREEN")); list.Add(GeneratePreset("Yellow", step: "YELLOW")); list.Add(GeneratePreset("Blue", step: "BLUE")); list.Add(GeneratePreset("Rakuten TV", appId: "ui30")); list.Add(GeneratePreset("Play", step: "PLAY")); list.Add(GeneratePreset("Pause", step: "PAUSE")); return list; } private LgPreset GeneratePreset(string name, string step = null, string appId = null, Keys key = Keys.None) { var preset = new LgPreset(); var cvt = new KeysConverter(); var shortcut = key != Keys.None ? (string)cvt.ConvertTo(key, typeof(string)) : null; preset.name = name; preset.appId = appId; preset.shortcut = shortcut; if (appId == null) { preset.steps.Add(step == null ? name : step); } return preset; } private void SavePresets() { try { var json = _JsonSerializer.Serialize(_presets); File.WriteAllText(_presetsFilename, json); } catch (Exception e) { Logger.Error(e.ToLogString()); } } private void SaveConfig() { Config.PowerOnAfterStartup = Devices?.Any(d => d.PowerOnAfterStartup) ?? false; var json = JsonConvert.SerializeObject(Config); File.WriteAllText(_configFilename, json); } private void SaveRemoteControlButtons() { var json = JsonConvert.SerializeObject(_remoteControlButtons); File.WriteAllText(_rcButtonsFilename, json); } public async Task RefreshDevices(bool connect = true, bool afterStartUp = false) { Devices = Config.Devices; var customIpAddresses = Devices.Where(d => d.IsCustom).Select(d => d.IpAddress); var pnpDevices = await Utils.GetPnpDevices(Config.DeviceSearchKey); var autoDevices = pnpDevices.Where(p => !customIpAddresses.Contains(p.IpAddress)).Select(d => new LgDevice(d.Name, d.IpAddress, d.MacAddress, false)).ToList(); var autoIpAddresses = pnpDevices.Select(d => d.IpAddress); Devices.RemoveAll(d => !d.IsCustom && !autoIpAddresses.Contains(d.IpAddress)); var newAutoDevices = autoDevices.Where(ad => !Devices.Any(d => d.IpAddress.Equals(ad.IpAddress))); Devices.AddRange(newAutoDevices); if (Devices.Any()) { var preferredDevice = Devices.FirstOrDefault(x => x.MacAddress != null && x.MacAddress.Equals(Config.PreferredMacAddress)) ?? Devices[0]; SelectedDevice = preferredDevice; } else { SelectedDevice = null; } if (afterStartUp && SelectedDevice == null && Config.PowerOnAfterStartup && !string.IsNullOrEmpty(Config.PreferredMacAddress)) { Logger.Debug("No device has been found, trying to wake it first..."); var tempDevice = new LgDevice("Test", string.Empty, Config.PreferredMacAddress); tempDevice.Wake(); await Task.Delay(4000); await RefreshDevices(); return; } if (connect && SelectedDevice != null) { if (_allowPowerOn) { WakeAfterStartupOrResume(); } else { var _ = SelectedDevice.Connect(); } } } public async Task ApplyPreset(string presetName) { var preset = _presets.FirstOrDefault(p => p.name.Equals(presetName, StringComparison.OrdinalIgnoreCase)); if (preset != null) { return await ApplyPreset(preset); } else { return false; } } public async Task ApplyPreset(LgPreset preset, bool reconnect = false) { var device = GetPresetDevice(preset); if (device == null) { Logger.Debug("Cannot apply preset: no device has been selected"); return false; } var result = await device.ExecutePreset(preset, reconnect, Config); _lastAppliedPreset = preset; return result; } public LgDevice GetPresetDevice(LgPreset preset) { var device = string.IsNullOrEmpty(preset.DeviceMacAddress) ? SelectedDevice : Devices.FirstOrDefault(d => d.MacAddress?.Equals(preset.DeviceMacAddress, StringComparison.OrdinalIgnoreCase) ?? false); return device; } public async Task GetMouseAsync() { return await SelectedDevice?.GetMouseAsync(); } public async Task RefreshApps(bool force = false) { if (SelectedDevice == null) { Logger.Debug("Cannot refresh apps: no device has been selected"); return; } var apps = await SelectedDevice.GetApps(force); if (apps.Any()) { _lgApps.Clear(); _lgApps.AddRange(apps); } } public List GetApps() { return _lgApps; } internal async Task PowerOff() { return await SelectedDevice?.PowerOff(); } internal Task PowerOffOnShutdownOrResume(PowerOnOffState state = PowerOnOffState.ShutDown) { var devices = Devices.Where(d => state == PowerOnOffState.ShutDown && d.PowerOffOnShutdown || state == PowerOnOffState.StandBy && d.PowerOffOnStandby); var tasks = new List(); foreach (var device in devices) { var task = device.PowerOff(); tasks.Add(task); } return Task.WhenAll(tasks.ToArray()); } internal bool WakeSelectedDevice() { return SelectedDevice?.Wake() ?? false; } internal void WakeAfterStartupOrResume(PowerOnOffState state = PowerOnOffState.StartUp, bool checkUserSession = true) { if (checkUserSession && !UserSessionInfo.UserLocalSession) { Logger.Debug($"WakeAfterStartupOrResume: not waking because session info indicates no local session"); return; } var wakeDevices = Devices.Where(d => state == PowerOnOffState.StartUp && d.PowerOnAfterStartup || state == PowerOnOffState.Resume && d.PowerOnAfterResume || state == PowerOnOffState.ScreenSaver && d.PowerSwitchOnScreenSaver); foreach (var device in wakeDevices) { if (device.PoweredOffBy == LgDevice.PowerOffSource.Manually) { Logger.Debug($"[{device.Name}]: device was manually powered off by user, not powering on"); continue; } device.DisposeConnection(); var _ = device.WakeAndConnectWithRetries(Config.PowerOnRetries); } } public void InstallEventHandlers() { SystemEvents.PowerModeChanged -= PowerModeChanged; SystemEvents.PowerModeChanged += PowerModeChanged; //SystemEvents.SessionEnded -= SessionEnded; //SystemEvents.SessionEnded += SessionEnded; UserSessionInfo.UserSessionSwitch -= SessionSwitched; UserSessionInfo.UserSessionSwitch += SessionSwitched; MonitorProcesses(); } private void SessionSwitched(bool toLocal) { if (toLocal) { if (_poweredOffByScreenSaver) { Logger.Debug("User switched to local and screen was powered off due to screensaver: waking up"); _poweredOffByScreenSaver = false; _poweredOffByScreenSaverProcessId = 0; WakeAfterStartupOrResume(); } else { Logger.Debug("User switched to local but screen was not powered off by screensaver: NOT waking up"); } } } private void PowerModeChanged(object sender, PowerModeChangedEventArgs e) { var powerOn = e.Mode == PowerModes.Resume; Logger.Debug($"PowerModeChanged: {e.Mode}"); if (powerOn) { WakeAfterStartupOrResume(PowerOnOffState.Resume); return; } if (Devices.Any(d => d.PowerOffOnStandby)) { NativeMethods.SetThreadExecutionState(NativeConstants.ES_CONTINUOUS | NativeConstants.ES_SYSTEM_REQUIRED | NativeConstants.ES_AWAYMODE_REQUIRED); try { var standByScript = Path.Combine(Program.DataDir, "StandByScript.bat"); if (File.Exists(standByScript)) { Utils.StartProcess(standByScript); } Logger.Debug("Powering off tv..."); var task = PowerOffOnShutdownOrResume(PowerOnOffState.StandBy); Utils.WaitForTask(task); Logger.Debug("Done powering off tv"); } finally { NativeMethods.SetThreadExecutionState(NativeConstants.ES_CONTINUOUS); } } } private void SessionEnded(object sender, SessionEndedEventArgs e) { //if (e.Reason == SessionEndReasons.SystemShutdown) //{ // Logger.Debug($"SessionEnded: {e.Reason}"); // if (Config.PowerOffOnShutdown) // { // PowerOff(); // } //} } private void MonitorProcesses() { var enableMonitoring = ShouldMonitorProcesses(); if (enableMonitoring && _monitorTask == null) { _monitorTask = CheckProcesses(); } else if (!enableMonitoring && _monitorTask != null) { _monitorTask = null; } } public bool ShouldMonitorProcesses() { return Devices.Any(d => d.PowerSwitchOnScreenSaver) || _presets.Any(p => p.Triggers.Any(t => t.Trigger != PresetTriggerType.None)); } public async Task CheckProcesses() { var wasConnected = Devices.Any(d => d.PowerSwitchOnScreenSaver && d.IsConnected()); _monitorTaskCounter++; var validCounter = _monitorTaskCounter; var lastProcessId = 0; var monitorContext = new ProcessMonitorContext(); //Process[] lastProcesses = null; while (validCounter == _monitorTaskCounter) { await Task.Delay(_poweredOffByScreenSaver ? 500 : 1000); try { var applicableDevices = Devices.Where(d => d.PowerSwitchOnScreenSaver || _presets.Any(p => p.Triggers.Any(t => t.Trigger != PresetTriggerType.None) && ((p.DeviceMacAddress == null && d == SelectedDevice) || p.DeviceMacAddress == d.MacAddress))); if (!applicableDevices.Any()) { continue; } if (_poweredOffByScreenSaver && !UserSessionInfo.UserLocalSession) { continue; } var processes = Process.GetProcesses(); if (_poweredOffByScreenSaver) { if (processes.Any(p => p.Id == _poweredOffByScreenSaverProcessId)) { continue; } Logger.Debug("Screensaver stopped, waking"); _poweredOffByScreenSaver = false; _poweredOffByScreenSaverProcessId = 0; lastProcessId = 0; WakeAfterStartupOrResume(PowerOnOffState.ScreenSaver); continue; } var connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); if (!connectedDevices.Any()) { if (wasConnected) { Logger.Debug("Process monitor: TV(s) where connected, but not any longer"); wasConnected = false; continue; } else { var devices = applicableDevices.ToList(); foreach (var device in devices) { Logger.Debug($"Process monitor: test connection with {device.Name}..."); var test = await device.TestConnection(); Logger.Debug("Process monitor: test connection result: " + test); } connectedDevices = applicableDevices.Where(d => d.IsConnected()).ToList(); if (!connectedDevices.Any()) { continue; } } } if (!wasConnected) { Logger.Debug("Process monitor: TV(s) where not connected, but connection has now been established"); wasConnected = true; } monitorContext.Devices = connectedDevices; monitorContext.RunningProcesses = processes; //if (lastProcesses != null) //{ // monitorContext.StartedProcesses = processes.Where(p => !lastProcesses.Any(lp => lp.Id == p.Id)).ToArray(); // monitorContext.StoppedProcesses = lastProcesses.Where(lp => !processes.Any(p => p.Id == lp.Id)).ToArray(); //} //else //{ // monitorContext.StartedProcesses = null; // monitorContext.StoppedProcesses = null; //} //if (monitorContext.StartedProcesses?.Any() ?? false) //{ // Logger.Debug("Started processes: " + string.Join(", ", monitorContext.StartedProcesses.Select(p => p.ProcessName))); //} //lastProcesses = processes; var process = processes.FirstOrDefault(p => p.ProcessName.ToLowerInvariant().EndsWith(".scr")); if (process != null) { var parent = process.Parent(); if (parent?.ProcessName.Contains("winlogon") ?? false) { Logger.Debug($"Screensaver started: {process.ProcessName}, parent: {parent.ProcessName}"); try { foreach (var device in connectedDevices) { Logger.Debug($"Screensaver check: test connection with {device.Name}..."); var test = await device.TestConnection(); Logger.Debug("Screensaver check: test connection result: " + test); if (!test) { continue; } Logger.Debug($"Screensaver check: powering off tv {device.Name} because of screensaver"); try { _poweredOffByScreenSaver = await device.PowerOff(); } catch (Exception pex) { // Assume powering off was successful _poweredOffByScreenSaver = true; Logger.Error($"Screensaver check: exception while powering off (probably connection closed): " + pex.Message); } Logger.Debug($"Screensaver check: powered off tv {device.Name}"); } _poweredOffByScreenSaverProcessId = process.Id; lastProcessId = 0; } catch (Exception e) { Logger.Error($"Screensaver check: can't power off: " + e.ToLogString()); } } else if (process.Id != lastProcessId) { Logger.Debug($"Screensaver started: {process.ProcessName}, but invalid parent: {parent?.ProcessName ?? "no parent"}"); lastProcessId = process.Id; } } else { await ExecuteProcessPresets(monitorContext); } } catch (Exception ex) { Logger.Error("CheckProcesses: " + ex.ToLogString()); } } } private async Task ExecuteProcessPresets(ProcessMonitorContext context) { var presets = _presets.Where(p => p.Triggers.Any(t => t.Trigger == PresetTriggerType.ProcessSwitch)); if (!presets.Any()) { return; } context.IsNotificationDisabled = Utils.IsNotificationDisabled(); var (processId, isFullScreen) = Utils.GetForegroundProcessIdAndIfFullScreen(); if (processId > 0) { context.ForegroundProcess = context.RunningProcesses.FirstOrDefault(p => p.Id == processId); context.ForegroundProcessIsFullScreen = isFullScreen; } else { context.ForegroundProcess = null; context.ForegroundProcessIsFullScreen = false; } var changedProcesses = new List(); if (context.ForegroundProcess != null) { changedProcesses.Add(context.ForegroundProcess); } if (context.ForegroundProcess != null && context.ForegroundProcessIsFullScreen) { if (!context.LastFullScreenProcessName.Equals(context.ForegroundProcess.ProcessName)) { context.LastFullScreenProcessName = context.ForegroundProcess.ProcessName; //Logger.Debug($"Foreground fullscreen app detected: {context.ForegroundProcess.ProcessName}"); } presets = presets.Where(p => p.Triggers.Any(t => t.Conditions.HasFlag(PresetConditionType.FullScreen))); context.WasFullScreen = true; } else if (context.WasFullScreen) { presets = presets.Where(p => p.Triggers.Any(t => !t.Conditions.HasFlag(PresetConditionType.FullScreen))); context.WasFullScreen = false; context.LastFullScreenProcessName = string.Empty; } var isHDRActive = context.Devices.Any(d => d.IsUsingHDRPictureMode()); var triggerContext = new PresetTriggerContext { IsHDRActive = isHDRActive, ForegroundProcess = context.ForegroundProcess, ForegroundProcessIsFullScreen = context.ForegroundProcessIsFullScreen, ChangedProcesses = changedProcesses, IsNotificationDisabled = context.IsNotificationDisabled }; var toApplyPresets = new List(); foreach (var preset in presets) { if (preset.Triggers.Any(t => t.TriggerActive(triggerContext))) { toApplyPresets.Add(preset); } } if (toApplyPresets.Any()) { LgPreset toApplyPreset; if (toApplyPresets.Count > 1) { toApplyPreset = toApplyPresets.FirstOrDefault(p => p.Triggers.Any(t => !t.IncludedProcesses.Contains("*"))); toApplyPreset = toApplyPreset != null ? toApplyPreset : toApplyPresets.First(); } else { toApplyPreset = toApplyPresets.First(); } if (_lastTriggeredPreset != toApplyPreset) { await ApplyPreset(toApplyPreset); _lastTriggeredPreset = toApplyPreset; } } } internal void AddCustomDevice(LgDevice device) { Devices.Add(device); } internal void RemoveCustomDevice(LgDevice device) { Devices.Remove(device); if (!string.IsNullOrEmpty(device.MacAddress)) { var presets = _presets.Where(p => !string.IsNullOrEmpty(p.DeviceMacAddress) && p.DeviceMacAddress.Equals(device.MacAddress)).ToList(); presets.ForEach(preset => { preset.DeviceMacAddress = null; }); } if (SelectedDevice == device) { SelectedDevice = Devices.Any() ? Devices.First() : null; } } } }