diff --git a/UserInterface/ViewModels/AddSensorViewModel.cs b/UserInterface/ViewModels/AddSensorViewModel.cs index 0ea2220..665e4ad 100644 --- a/UserInterface/ViewModels/AddSensorViewModel.cs +++ b/UserInterface/ViewModels/AddSensorViewModel.cs @@ -14,9 +14,11 @@ namespace UserInterface.ViewModels public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); } public bool ShowQueryInput { get => showQueryInput; set => this.RaiseAndSetIfChanged(ref showQueryInput, value); } + public bool ShowWindowNameInput { get => showWindowNameInput; set => this.RaiseAndSetIfChanged(ref showWindowNameInput, value); } private string moreInfoLink; private int updateInterval; + private bool showWindowNameInput; public string MoreInfoLink { @@ -28,6 +30,7 @@ namespace UserInterface.ViewModels public AvailableSensors SelectedType { get => selectedType; set => this.RaiseAndSetIfChanged(ref selectedType, value); } public string Name { get; set; } public string Query { get; set; } + public string WindowName { get; set; } public int UpdateInterval { get => updateInterval; set => this.RaiseAndSetIfChanged(ref updateInterval, value); } } } diff --git a/UserInterface/Views/AddSensorDialog.axaml b/UserInterface/Views/AddSensorDialog.axaml index 7dff3e4..e80f016 100644 --- a/UserInterface/Views/AddSensorDialog.axaml +++ b/UserInterface/Views/AddSensorDialog.axaml @@ -23,6 +23,9 @@ Query + Window name + This is case-insensitive and loosely matched. A window called "Spotify Premium" will match "spotify" or "premium". + diff --git a/UserInterface/Views/AddSensorDialog.axaml.cs b/UserInterface/Views/AddSensorDialog.axaml.cs index 3e31aef..ba83103 100644 --- a/UserInterface/Views/AddSensorDialog.axaml.cs +++ b/UserInterface/Views/AddSensorDialog.axaml.cs @@ -47,7 +47,7 @@ namespace UserInterface.Views public async void Save(object sender, RoutedEventArgs args) { var item = ((AddSensorViewModel)this.DataContext); - dynamic model = new { Name = item.Name, Query = item.Query, UpdateInterval = item.UpdateInterval }; + dynamic model = new { item.Name, item.Query, item.UpdateInterval, item.WindowName }; string json = JsonSerializer.Serialize(model); await this.client.InvokeAsync(x => x.AddSensor(item.SelectedType, json)); Close(); @@ -62,42 +62,56 @@ namespace UserInterface.Views item.Description = "This sensor watches the UserNotificationState. This is normally used in applications to determine if it is appropriate to send a notification but we can use it to expose this state. \n "; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#usernotificationstate"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; case AvailableSensors.DummySensor: item.Description = "This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#dummy"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; item.UpdateInterval = 1; break; case AvailableSensors.CPULoadSensor: item.Description = "This sensor checks the current CPU load. It averages the load on all logical cores every second and rounds the output to two decimals."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#cpuload"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; case AvailableSensors.CurrentClockSpeedSensor: item.Description = "This sensor returns the BIOS configured baseclock for the processor."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#currentclockspeed"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; item.UpdateInterval = 3600; break; case AvailableSensors.WMIQuerySensor: item.Description = "This advanced sensor executes a user defined WMI query and exposes the result. The query should return a single value."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#wmiquerysensor"; item.ShowQueryInput = true; + item.ShowWindowNameInput = false; item.UpdateInterval = 10; break; case AvailableSensors.MemoryUsageSensor: item.Description = "This sensor calculates the percentage of used memory."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#usedmemorysensor"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; item.UpdateInterval = 10; break; case AvailableSensors.ActiveWindowSensor: item.Description = "This sensor exposes the name of the currently active window."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#activewindow"; item.ShowQueryInput = false; + item.ShowWindowNameInput = false; + item.UpdateInterval = 5; + break; + case AvailableSensors.NamedWindowSensor: + item.Description = "This sensor returns true if a window was found with the name you search for. "; + item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#namedwindow"; + item.ShowQueryInput = false; + item.ShowWindowNameInput = true; item.UpdateInterval = 5; break; default: diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs index 64f77c7..dea8726 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs @@ -106,6 +106,9 @@ namespace hass_workstation_service.Communication.InterProcesCommunication case AvailableSensors.ActiveWindowSensor: sensorToCreate = new ActiveWindowSensor(this._publisher, (int)model.UpdateInterval, model.Name); break; + case AvailableSensors.NamedWindowSensor: + sensorToCreate = new NamedWindowSensor(this._publisher, model.WindowName, model.Name, (int)model.UpdateInterval); + break; default: Log.Logger.Error("Unknown sensortype"); break; diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs index d6e73f2..9fc20e5 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs @@ -36,6 +36,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models CPULoadSensor, WMIQuerySensor, MemoryUsageSensor, - ActiveWindowSensor + ActiveWindowSensor, + NamedWindowSensor } } diff --git a/hass-workstation-service/Data/ConfigurationService.cs b/hass-workstation-service/Data/ConfigurationService.cs index daee9a9..d46bd6c 100644 --- a/hass-workstation-service/Data/ConfigurationService.cs +++ b/hass-workstation-service/Data/ConfigurationService.cs @@ -88,6 +88,9 @@ namespace hass_workstation_service.Data case "ActiveWindowSensor": sensor = new ActiveWindowSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; + case "NamedWindowSensor": + sensor = new NamedWindowSensor(publisher, configuredSensor.WindowName, configuredSensor.Name, configuredSensor.UpdateInterval, configuredSensor.Id); + break; default: Log.Logger.Error("unsupported sensor type in config"); break; @@ -164,6 +167,11 @@ namespace hass_workstation_service.Data var wmiSensor = (WMIQuerySensor)sensor; configuredSensorsToSave.Add(new ConfiguredSensor() { Id = wmiSensor.Id, Name = wmiSensor.Name, Type = wmiSensor.GetType().Name, UpdateInterval = wmiSensor.UpdateInterval, Query = wmiSensor.Query }); } + if (sensor is NamedWindowSensor) + { + var namedWindowSensor = (NamedWindowSensor)sensor; + configuredSensorsToSave.Add(new ConfiguredSensor() { Id = namedWindowSensor.Id, Name = namedWindowSensor.Name, Type = namedWindowSensor.GetType().Name, UpdateInterval = namedWindowSensor.UpdateInterval, WindowName = namedWindowSensor.WindowName }); + } else { configuredSensorsToSave.Add(new ConfiguredSensor() { Id = sensor.Id, Name = sensor.Name, Type = sensor.GetType().Name, UpdateInterval = sensor.UpdateInterval }); diff --git a/hass-workstation-service/Data/ConfiguredSensor.cs b/hass-workstation-service/Data/ConfiguredSensor.cs index 06dfd50..fd629dd 100644 --- a/hass-workstation-service/Data/ConfiguredSensor.cs +++ b/hass-workstation-service/Data/ConfiguredSensor.cs @@ -9,5 +9,6 @@ namespace hass_workstation_service.Data public string Name { get; set; } public string Query { get; set; } public int? UpdateInterval { get; set; } + public string WindowName { get; set; } } } \ No newline at end of file diff --git a/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs new file mode 100644 index 0000000..fe6c620 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs @@ -0,0 +1,81 @@ +using hass_workstation_service.Communication; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using HWND = System.IntPtr; + +namespace hass_workstation_service.Domain.Sensors +{ + public class NamedWindowSensor : AbstractSensor + { + public string WindowName { get; protected set; } + public NamedWindowSensor(MqttPublisher publisher, string windowName, string name = "NamedWindow", int? updateInterval = 10, Guid id = default) : base(publisher, name ?? "NamedWindow", updateInterval ?? 10, id) + { + this.WindowName = windowName; + } + + public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + { + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + { + Name = this.Name, + Unique_id = this.Id.ToString(), + Device = this.Publisher.DeviceConfigModel, + State_topic = $"homeassistant/sensor/{this.Name}/state", + Icon = "mdi:window-maximize", + }); + } + + public override string GetState() + { + var windowNames = GetOpenWindows().Values; + return windowNames.Any(v => v.Contains(this.WindowName, StringComparison.OrdinalIgnoreCase)) ? "True" : "False"; + } + + + /// Returns a dictionary that contains the handle and title of all the open windows. + /// A dictionary that contains the handle and title of all the open windows. + public static IDictionary GetOpenWindows() + { + HWND shellWindow = GetShellWindow(); + Dictionary windows = new Dictionary(); + + EnumWindows(delegate (HWND hWnd, int lParam) + { + if (hWnd == shellWindow) return true; + if (!IsWindowVisible(hWnd)) return true; + + int length = GetWindowTextLength(hWnd); + if (length == 0) return true; + + StringBuilder builder = new StringBuilder(length); + GetWindowText(hWnd, builder, length + 1); + + windows[hWnd] = builder.ToString(); + return true; + + }, 0); + + return windows; + } + + private delegate bool EnumWindowsProc(HWND hWnd, int lParam); + + [DllImport("USER32.DLL")] + private static extern bool EnumWindows(EnumWindowsProc enumFunc, int lParam); + + [DllImport("USER32.DLL")] + private static extern int GetWindowText(HWND hWnd, StringBuilder lpString, int nMaxCount); + + [DllImport("USER32.DLL")] + private static extern int GetWindowTextLength(HWND hWnd); + + [DllImport("USER32.DLL")] + private static extern bool IsWindowVisible(HWND hWnd); + + [DllImport("USER32.DLL")] + private static extern IntPtr GetShellWindow(); + } +}