diff --git a/UserInterface/UserInterface.csproj b/UserInterface/UserInterface.csproj index 73c7ed9..343270c 100644 --- a/UserInterface/UserInterface.csproj +++ b/UserInterface/UserInterface.csproj @@ -17,7 +17,7 @@ - + diff --git a/UserInterface/ViewModels/AddSensorViewModel.cs b/UserInterface/ViewModels/AddSensorViewModel.cs index 4e22e0f..f458e82 100644 --- a/UserInterface/ViewModels/AddSensorViewModel.cs +++ b/UserInterface/ViewModels/AddSensorViewModel.cs @@ -13,6 +13,7 @@ namespace UserInterface.ViewModels private bool _showWindowNameInput; private string _moreInfoLink; private string _query; + private string _scope; private string _windowName; public AvailableSensors SelectedType { get => _selectedType; set => this.RaiseAndSetIfChanged(ref _selectedType, value); } @@ -23,6 +24,7 @@ namespace UserInterface.ViewModels public bool ShowWindowNameInput { get => _showWindowNameInput; set => this.RaiseAndSetIfChanged(ref _showWindowNameInput, value); } public string MoreInfoLink { get => _moreInfoLink; set => this.RaiseAndSetIfChanged(ref _moreInfoLink, value); } public string Query { get => _query; set => this.RaiseAndSetIfChanged(ref _query, value); } + public string Scope { get => _scope; set => this.RaiseAndSetIfChanged(ref _scope, value); } public string WindowName { get => _windowName; set => this.RaiseAndSetIfChanged(ref _windowName, value); } } } \ No newline at end of file diff --git a/UserInterface/ViewModels/BrokerSettingsViewModel.cs b/UserInterface/ViewModels/BrokerSettingsViewModel.cs index 2811cb3..5c999d6 100644 --- a/UserInterface/ViewModels/BrokerSettingsViewModel.cs +++ b/UserInterface/ViewModels/BrokerSettingsViewModel.cs @@ -16,6 +16,9 @@ namespace UserInterface.ViewModels private bool isConnected; private int? port; private bool useTLS; + private bool retainLWT = true; + private string rootCaPath; + private string clientCertPath; public bool IsConnected { get => isConnected; set => this.RaiseAndSetIfChanged(ref isConnected, value); } public string Message { get => message; set => this.RaiseAndSetIfChanged(ref message, value); } @@ -29,6 +32,13 @@ namespace UserInterface.ViewModels public bool UseTLS { get => useTLS; set => this.RaiseAndSetIfChanged(ref useTLS, value); } + public bool RetainLWT { get => retainLWT; set => this.RaiseAndSetIfChanged(ref retainLWT, value); } + + public string RootCAPath { get => rootCaPath; set => this.RaiseAndSetIfChanged(ref rootCaPath, value); } + + public string ClientCertPath { get => clientCertPath; set => this.RaiseAndSetIfChanged(ref clientCertPath, value); } + + public void Update(MqttSettings settings) { this.Host = settings.Host; @@ -36,6 +46,9 @@ namespace UserInterface.ViewModels this.Password = settings.Password; this.Port = settings.Port; this.UseTLS = settings.UseTLS; + this.RetainLWT = settings.RetainLWT; + this.RootCAPath = settings.RootCAPath; + this.ClientCertPath = settings.ClientCertPath; } public void UpdateStatus(MqqtClientStatus status) diff --git a/UserInterface/Views/AddSensorDialog.axaml b/UserInterface/Views/AddSensorDialog.axaml index 8b3b295..865ab19 100644 --- a/UserInterface/Views/AddSensorDialog.axaml +++ b/UserInterface/Views/AddSensorDialog.axaml @@ -21,8 +21,10 @@ - Query - + Scope (optional) + + 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 9ab9bde..ef1a29a 100644 --- a/UserInterface/Views/AddSensorDialog.axaml.cs +++ b/UserInterface/Views/AddSensorDialog.axaml.cs @@ -67,6 +67,7 @@ namespace UserInterface.Views item.Name = sensor.Name; item.UpdateInterval = sensor.UpdateInterval; item.Query = sensor.Query; + item.Scope = sensor.Scope; item.WindowName = sensor.WindowName; Title = $"Edit {sensor.Name}"; @@ -75,7 +76,7 @@ namespace UserInterface.Views public async void Save(object sender, RoutedEventArgs args) { var item = (AddSensorViewModel)DataContext; - dynamic model = new { item.Name, item.Query, item.UpdateInterval, item.WindowName }; + dynamic model = new { item.Name, item.Query, item.UpdateInterval, item.WindowName, item.Scope }; string json = JsonSerializer.Serialize(model); if (SensorId == Guid.Empty) await _client.InvokeAsync(x => x.AddSensor(item.SelectedType, json)); @@ -102,7 +103,7 @@ namespace UserInterface.Views 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/blob/master/documentation/Sensors.md#dummysensor"; @@ -110,7 +111,7 @@ namespace UserInterface.Views 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/blob/master/documentation/Sensors.md#cpuloadsensor"; @@ -118,7 +119,7 @@ namespace UserInterface.Views 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/blob/master/documentation/Sensors.md#currentclockspeedsensor"; @@ -126,7 +127,7 @@ namespace UserInterface.Views 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/sleevezipperhass-workstation-service/blob/master/documentation/WMIQuery.md#wmiquerysensor"; @@ -134,7 +135,7 @@ namespace UserInterface.Views 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/blob/master/documentation/Sensors.md#memoryusagesensorsensor"; @@ -142,7 +143,7 @@ namespace UserInterface.Views 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/blob/master/documentation/Sensors.md#activewindowsensor"; @@ -150,28 +151,35 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.WebcamActiveSensor: item.Description = "This sensor shows if the webcam is currently being used."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#webcamactivesensor"; item.ShowQueryInput = false; item.UpdateInterval = 10; break; - + case AvailableSensors.WebcamProcessSensor: item.Description = "This sensor shows which process is using the webcam."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#webcamprocesssensor"; item.ShowQueryInput = false; item.UpdateInterval = 10; break; - + case AvailableSensors.MicrophoneActiveSensor: item.Description = "This sensor shows if the microphone is currently in use."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#microphoneactivesensor"; item.ShowQueryInput = false; item.UpdateInterval = 10; break; - + + case AvailableSensors.MicrophoneProcessSensor: + item.Description = "This sensor shows which process is using the microphone."; + item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#microphoneprocesssensor"; + item.ShowQueryInput = false; + item.UpdateInterval = 10; + 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/blob/master/documentation/Sensors.md#namedwindowsensor"; @@ -179,7 +187,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = true; item.UpdateInterval = 5; break; - + case AvailableSensors.LastActiveSensor: item.Description = "This sensor returns the date/time that the workstation was last active."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#lastactivesensor"; @@ -187,7 +195,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.LastBootSensor: item.Description = "This sensor returns the date/time that Windows was last booted"; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#lastbootsensor"; @@ -195,7 +203,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.SessionStateSensor: item.Description = "This sensor returns the state of the Windows session."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#sessionstatesensor"; @@ -203,7 +211,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.CurrentVolumeSensor: item.Description = "This sensor returns the volume of currently playing audio."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#currentvolumesensor"; @@ -211,7 +219,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.MasterVolumeSensor: item.Description = "This sensor returns the master volume of the currently selected default audio device as a percentage value."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#mastervolumesensor"; @@ -219,7 +227,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.GPUTemperatureSensor: item.Description = "This sensor returns the current temperature of the GPU in °C."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#gputemperaturesensor"; @@ -227,7 +235,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + case AvailableSensors.GPULoadSensor: item.Description = "This sensor returns the current GPU load."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#gpuloadsensor"; @@ -235,7 +243,7 @@ namespace UserInterface.Views item.ShowWindowNameInput = false; item.UpdateInterval = 5; break; - + default: item.Description = null; item.MoreInfoLink = null; diff --git a/UserInterface/Views/BrokerSettings.axaml b/UserInterface/Views/BrokerSettings.axaml index b23b798..becb04e 100644 --- a/UserInterface/Views/BrokerSettings.axaml +++ b/UserInterface/Views/BrokerSettings.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="UserInterface.Views.BrokerSettings"> - + MQTT Broker @@ -25,6 +25,65 @@ Password + + + + + + Retain LastWillAndTestament + + (What's this?) + + + +[Experimental] +If set, sets Retain on the Last Will and Testament message. +Only turn this off if you use a broker that does not support this(e.g. AWS IoT Core) +Defaults to True + + + + + + + + + + Root Cert Path (.pem/.crt) + + (What's this?) + + + +[Experimental] +If set, use this certificate in the TLS configuration for the MQTT connection. +This will be a pem or crt file provided by your broker. + + + + + + + + + Client Cert Path (.pfx) + + (What's this?) + + + +[Experimental] +If set, use this certificate in the TLS configuration for the MQTT connection. +This should be the private key .pfx file for a device created in your broker corresponding to this Windows PC. + + + + + + + + + diff --git a/UserInterface/Views/BrokerSettings.axaml.cs b/UserInterface/Views/BrokerSettings.axaml.cs index e870a80..746b44d 100644 --- a/UserInterface/Views/BrokerSettings.axaml.cs +++ b/UserInterface/Views/BrokerSettings.axaml.cs @@ -50,7 +50,7 @@ namespace UserInterface.Views ICollection results; if (model.IsValid(model, out results)) { - var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS })); + var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS, RootCAPath = model.RootCAPath, ClientCertPath = model.ClientCertPath, RetainLWT = model.RetainLWT })); } } diff --git a/UserInterface/Views/GeneralSettings.axaml b/UserInterface/Views/GeneralSettings.axaml index 4ca197d..47259c2 100644 --- a/UserInterface/Views/GeneralSettings.axaml +++ b/UserInterface/Views/GeneralSettings.axaml @@ -4,22 +4,22 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450" x:Class="UserInterface.Views.GeneralSettingsView"> - + Settings Name prefix - (What's this?) - - - + (What's this?) + + + [Experimental] This allows you to set a name which will be used to prefix all sensor- and command names. For example: If a sensor is called "ActiveWindow" and the name prefix is set to "laptop", the sensor will be named "laptop-ActiveWindow" and its entityId will be "laptop_activewindow". - + - - - + + + diff --git a/UserInterface/Views/MainWindow.axaml b/UserInterface/Views/MainWindow.axaml index 31d7da7..c9cfe81 100644 --- a/UserInterface/Views/MainWindow.axaml +++ b/UserInterface/Views/MainWindow.axaml @@ -14,17 +14,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/documentation/Sensors.md b/documentation/Sensors.md index 2938c7d..6d35695 100644 --- a/documentation/Sensors.md +++ b/documentation/Sensors.md @@ -24,7 +24,7 @@ This sensor returns the current volume of playing audio. **It does not return th This sensor returns the master volume for the currently selected default audio device. -### DummySensor +### DummySensor This sensor produces a random output every second, and is intended to test latency and connectivity. @@ -89,6 +89,10 @@ The webcam active sensor returns the status of the webcam. The webcam process sensor returns the process which is using the webcam. +### MicrophoneProcessSensor + +The microphone process sensor returns the process which is using the microphone. + ### WMIQuerySensor Please see the specific documentaion page [here](https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/WMIQuery.md#wmiquerysensor). diff --git a/documentation/WMIQuery.md b/documentation/WMIQuery.md index fe4277d..b786bd8 100644 --- a/documentation/WMIQuery.md +++ b/documentation/WMIQuery.md @@ -13,7 +13,7 @@ The command ```sql SELECT * FROM Win32_Processor``` cannot be used because it re You can use [WMI Explorer](https://github.com/vinaypamnani/wmie2/releases) to construct a query, or alternatively look at the user submitted sensors below - +If a class or value cannot be found in the default scope, you can use the "Scope" setting when adding or editing the sensor. --- ## User Submitted Sensor Examples diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs index 9c9824e..bc26f5b 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs @@ -30,7 +30,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication /// /// You can use this to check if the application responds. - /// + /// /// /// public string Ping(string str) => str == "ping" ? "pong" : "what?"; @@ -44,7 +44,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication public void WriteMqttBrokerSettingsAsync(MqttSettings settings) => _configurationService.WriteMqttBrokerSettingsAsync(settings); /// - /// Enables or disables autostart. + /// Enables or disables autostart. /// /// public void EnableAutostart(bool enable) => _configurationService.EnableAutoStart(enable); @@ -108,7 +108,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication public void RemoveCommandById(Guid id) => _configurationService.DeleteConfiguredCommand(id); /// - /// Adds a command to the configured commands. This properly initializes the class and writes it to the config file. + /// Adds a command to the configured commands. This properly initializes the class and writes it to the config file. /// /// /// @@ -123,7 +123,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication } /// - /// Adds a command to the configured commands. This properly initializes the class, subscribes to the command topic and writes it to the config file. + /// Adds a command to the configured commands. This properly initializes the class, subscribes to the command topic and writes it to the config file. /// /// /// @@ -173,12 +173,13 @@ namespace hass_workstation_service.Communication.InterProcesCommunication AvailableSensors.DummySensor => new DummySensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.CurrentClockSpeedSensor => new CurrentClockSpeedSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.CPULoadSensor => new CPULoadSensor(_publisher, (int)model.UpdateInterval, model.Name), - AvailableSensors.WMIQuerySensor => new WMIQuerySensor(_publisher, model.Query, (int)model.UpdateInterval, model.Name), + AvailableSensors.WMIQuerySensor => new WMIQuerySensor(_publisher, model.Query, (int)model.UpdateInterval, model.Name, scope: model.Scope), AvailableSensors.MemoryUsageSensor => new MemoryUsageSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.ActiveWindowSensor => new ActiveWindowSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.WebcamActiveSensor => new WebcamActiveSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.WebcamProcessSensor => new WebcamProcessSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.MicrophoneActiveSensor => new MicrophoneActiveSensor(_publisher, (int)model.UpdateInterval, model.Name), + AvailableSensors.MicrophoneProcessSensor => new MicrophoneProcessSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.NamedWindowSensor => new NamedWindowSensor(_publisher, model.WindowName, model.Name, (int)model.UpdateInterval), AvailableSensors.LastActiveSensor => new LastActiveSensor(_publisher, (int)model.UpdateInterval, model.Name), AvailableSensors.LastBootSensor => new LastBootSensor(_publisher, (int)model.UpdateInterval, model.Name), diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs index fafa58d..e803f52 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs @@ -11,6 +11,10 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models public string Password { get; set; } public int? Port { get; set; } public bool UseTLS { get; set; } + + public bool RetainLWT { get; set; } + public string RootCAPath { get; set; } + public string ClientCertPath { get; set; } } public class MqqtClientStatus @@ -26,6 +30,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models public string Name { get; set; } public string Value { get; set; } public string Query { get; set; } + public string Scope { get; set; } public string WindowName { get; set; } public int UpdateInterval { get; set; } public string UnitOfMeasurement { get; set; } @@ -40,12 +45,13 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models if (sensor is WMIQuerySensor wMIQuerySensor) { this.Query = wMIQuerySensor.Query; + this.Scope = wMIQuerySensor.Scope; } if (sensor is NamedWindowSensor namedWindowSensor) { this.WindowName = namedWindowSensor.WindowName; } - this.UpdateInterval = sensor.UpdateInterval; + this.UpdateInterval = sensor.UpdateInterval; this.UnitOfMeasurement = ((SensorDiscoveryConfigModel)sensor.GetAutoDiscoveryConfig()).Unit_of_measurement; } public ConfiguredSensorModel() @@ -95,6 +101,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models WebcamActiveSensor, WebcamProcessSensor, MicrophoneActiveSensor, + MicrophoneProcessSensor, ActiveWindowSensor, NamedWindowSensor, LastActiveSensor, diff --git a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs index 7449d67..0cacde5 100644 --- a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs +++ b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs @@ -116,7 +116,7 @@ namespace hass_workstation_service.Communication var message = new MqttApplicationMessageBuilder() .WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/config") .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(discoverable.GetAutoDiscoveryConfig(), discoverable.GetAutoDiscoveryConfig().GetType(), options)) - .WithRetainFlag() + //.WithRetainFlag() .Build(); await this.Publish(message); // if clearconfig is true, also remove previous state messages @@ -125,7 +125,7 @@ namespace hass_workstation_service.Communication var stateMessage = new MqttApplicationMessageBuilder() .WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/state") .WithPayload("") - .WithRetainFlag() + // .WithRetainFlag() .Build(); await this.Publish(stateMessage); } diff --git a/hass-workstation-service/Data/ConfigurationService.cs b/hass-workstation-service/Data/ConfigurationService.cs index 3ef2a15..88ba77c 100644 --- a/hass-workstation-service/Data/ConfigurationService.cs +++ b/hass-workstation-service/Data/ConfigurationService.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; +using System.Security.Cryptography.X509Certificates; using System.Text.Json; using System.Threading.Tasks; using hass_workstation_service.Communication; @@ -20,6 +21,7 @@ using MQTTnet.Client.Options; using MQTTnet.Extensions.ManagedClient; using Serilog; + namespace hass_workstation_service.Data { public class ConfigurationService : IConfigurationService @@ -135,6 +137,9 @@ namespace hass_workstation_service.Data case "MicrophoneActiveSensor": sensor = new MicrophoneActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; + case "MicrophoneProcessSensor": + sensor = new MicrophoneProcessSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; case "SessionStateSensor": sensor = new SessionStateSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; @@ -152,7 +157,7 @@ namespace hass_workstation_service.Data break; // keep this one last! case "WMIQuerySensor": - sensor = new WMIQuerySensor(publisher, configuredSensor.Query, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + sensor = new WMIQuerySensor(publisher, configuredSensor.Query, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id, configuredSensor.Scope); break; default: Log.Logger.Error("unsupported sensor type in config"); @@ -205,22 +210,22 @@ namespace hass_workstation_service.Data case "CustomCommand": command = new CustomCommand(publisher, configuredCommand.Command, configuredCommand.Name, configuredCommand.Id); break; - case "MediaPlayPauseCommand": + case "PlayPauseCommand": command = new PlayPauseCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; - case "MediaNextCommand": + case "NextCommand": command = new NextCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; - case "MediaPreviousCommand": + case "PreviousCommand": command = new PreviousCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; - case "MediaVolumeUpCommand": + case "VolumeUpCommand": command = new VolumeUpCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; - case "MediaVolumeDownCommand": + case "VolumeDownCommand": command = new VolumeDownCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; - case "MediaMuteCommand": + case "MuteCommand": command = new MuteCommand(publisher, configuredCommand.Name, configuredCommand.Id); break; case "KeyCommand": @@ -307,22 +312,54 @@ namespace hass_workstation_service.Data if (configuredBroker != null && configuredBroker.Host != null) { - var mqttClientOptions = new MqttClientOptionsBuilder() + + var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() .WithTcpServer(configuredBroker.Host, configuredBroker.Port) - .WithTls(new MqttClientOptionsBuilderTlsParameters() - { - UseTls = configuredBroker.UseTLS, - AllowUntrustedCertificates = true, - SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None - }) .WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString()) - .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) - .WithWillMessage(new MqttApplicationMessageBuilder() - .WithRetainFlag() + .WithKeepAlivePeriod(TimeSpan.FromSeconds(30)); + + + /* Start LWT */ + var lwtMessage = new MqttApplicationMessageBuilder() .WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability") - .WithPayload("offline") - .Build()) - .Build(); + .WithPayload("offline"); + if (configuredBroker.RetainLWT) { + lwtMessage.WithRetainFlag(); + } + + mqttClientOptionsBuilder.WithWillMessage(lwtMessage.Build()); + /* End LWT */ + + + /* Start TLS/Certificate configuration */ + + var tlsParameters = new MqttClientOptionsBuilderTlsParameters() + { + UseTls = configuredBroker.UseTLS, + AllowUntrustedCertificates = true, + SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None + }; + + var certs = new List(); + + if (!string.IsNullOrEmpty(configuredBroker.RootCAPath)) { + certs.Add(new X509Certificate2(configuredBroker.RootCAPath)); + } + + if (!string.IsNullOrEmpty(configuredBroker.ClientCertPath)) + { + certs.Add(new X509Certificate2(configuredBroker.ClientCertPath)); + } + if (certs.Count > 0) { + // IF certs are configured, let's add them here + tlsParameters.Certificates = certs; + } + mqttClientOptionsBuilder.WithTls(tlsParameters); + + /* End TLS/Certificate Configuration */ + + + var mqttClientOptions = mqttClientOptionsBuilder.Build(); return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build(); } else @@ -375,7 +412,7 @@ namespace hass_workstation_service.Data if (sensor is WMIQuerySensor wmiSensor) { #pragma warning disable CA1416 // Validate platform compatibility. We ignore it here because this would never happen. A cleaner solution may be implemented later. - configuredSensorsToSave.Add(new ConfiguredSensor() { Id = wmiSensor.Id, Name = wmiSensor.Name, Type = wmiSensor.GetType().Name, UpdateInterval = wmiSensor.UpdateInterval, Query = wmiSensor.Query }); + configuredSensorsToSave.Add(new ConfiguredSensor() { Id = wmiSensor.Id, Name = wmiSensor.Name, Type = wmiSensor.GetType().Name, UpdateInterval = wmiSensor.UpdateInterval, Query = wmiSensor.Query, Scope = wmiSensor.Scope }); #pragma warning restore CA1416 // Validate platform compatibility } else if (sensor is NamedWindowSensor namedWindowSensor) @@ -462,7 +499,7 @@ namespace hass_workstation_service.Data } /// - /// + /// /// /// The Id of the sensor to replace /// The new sensor @@ -541,7 +578,10 @@ namespace hass_workstation_service.Data Username = settings.Username, Password = settings.Password ?? "", Port = settings.Port ?? 1883, - UseTLS = settings.UseTLS + UseTLS = settings.UseTLS, + RetainLWT = settings.RetainLWT, + RootCAPath = settings.RootCAPath, + ClientCertPath = settings.ClientCertPath }; await JsonSerializer.SerializeAsync(stream, configuredBroker); @@ -560,7 +600,10 @@ namespace hass_workstation_service.Data Username = broker?.Username, Password = broker?.Password, Port = broker?.Port, - UseTLS = broker?.UseTLS ?? false + UseTLS = broker?.UseTLS ?? false, + RetainLWT = broker?.RetainLWT ?? true, + RootCAPath = broker?.RootCAPath, + ClientCertPath = broker?.RootCAPath }; } diff --git a/hass-workstation-service/Data/ConfiguredMqttBroker.cs b/hass-workstation-service/Data/ConfiguredMqttBroker.cs index f188322..d4a73d1 100644 --- a/hass-workstation-service/Data/ConfiguredMqttBroker.cs +++ b/hass-workstation-service/Data/ConfiguredMqttBroker.cs @@ -8,11 +8,33 @@ namespace hass_workstation_service.Data private string username; private string password; private int? port; + + private string rootCAPath; + private string clientCertPath; public string Host { get; set; } public int Port { get => port ?? 1883; set => port = value; } public bool UseTLS { get; set; } + // Before this option, Retains was the default, so let's keep that here to not break backwards compatibility + public bool RetainLWT { get; set; } = true; + + public string RootCAPath { + get + { + if (rootCAPath!= null) return rootCAPath; + return ""; + } + set => rootCAPath = value; + } + public string ClientCertPath { + get + { + if (clientCertPath != null) return clientCertPath; + return ""; + } + set => clientCertPath = value; + } public string Username { diff --git a/hass-workstation-service/Data/ConfiguredSensor.cs b/hass-workstation-service/Data/ConfiguredSensor.cs index dcb29eb..7a18d87 100644 --- a/hass-workstation-service/Data/ConfiguredSensor.cs +++ b/hass-workstation-service/Data/ConfiguredSensor.cs @@ -9,6 +9,7 @@ namespace hass_workstation_service.Data public Guid Id { get; set; } public string Name { get; set; } public string Query { get; set; } + public string Scope { get; set; } public int? UpdateInterval { get; set; } public string WindowName { get; set; } } diff --git a/hass-workstation-service/Domain/Commands/AbstractCommand.cs b/hass-workstation-service/Domain/Commands/AbstractCommand.cs index 94df49d..4e42b86 100644 --- a/hass-workstation-service/Domain/Commands/AbstractCommand.cs +++ b/hass-workstation-service/Domain/Commands/AbstractCommand.cs @@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Commands var message = new MqttApplicationMessageBuilder() .WithTopic(GetAutoDiscoveryConfig().State_topic) .WithPayload(state) - .WithExactlyOnceQoS() - .WithRetainFlag() + //.WithExactlyOnceQoS() + //.WithRetainFlag() .Build(); await Publisher.Publish(message); PreviousPublishedState = state; diff --git a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs index 73904bf..4aa8f7c 100644 --- a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs +++ b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs @@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Sensors var message = new MqttApplicationMessageBuilder() .WithTopic(GetAutoDiscoveryConfig().State_topic) .WithPayload(state) - .WithExactlyOnceQoS() - .WithRetainFlag() + //.WithExactlyOnceQoS() + //.WithRetainFlag() .Build(); await Publisher.Publish(message); PreviousPublishedState = state; diff --git a/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs b/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs index 9eba3b8..0405377 100644 --- a/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs +++ b/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs @@ -6,7 +6,7 @@ namespace hass_workstation_service.Domain.Sensors { public class LastActiveSensor : AbstractSensor { - + private DateTime _lastActive = DateTime.MinValue; public LastActiveSensor(MqttPublisher publisher, int? updateInterval = 10, string name = "LastActive", Guid id = default) : base(publisher, name ?? "LastActive", updateInterval ?? 10, id){} public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() @@ -25,7 +25,12 @@ namespace hass_workstation_service.Domain.Sensors public override string GetState() { - return GetLastInputTime().ToString("o", System.Globalization.CultureInfo.InvariantCulture); + var lastInput = GetLastInputTime(); + if ((_lastActive - lastInput).Duration().TotalSeconds > 1) + { + _lastActive = lastInput; + } + return _lastActive.ToString("o", System.Globalization.CultureInfo.InvariantCulture); } diff --git a/hass-workstation-service/Domain/Sensors/MicrophoneProcessSensor.cs b/hass-workstation-service/Domain/Sensors/MicrophoneProcessSensor.cs new file mode 100644 index 0000000..e266194 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/MicrophoneProcessSensor.cs @@ -0,0 +1,96 @@ +using hass_workstation_service.Communication; +using Microsoft.Win32; +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Collections.Generic; + +namespace hass_workstation_service.Domain.Sensors +{ + public class MicrophoneProcessSensor : AbstractSensor + { + private HashSet processes = new HashSet(); + + public MicrophoneProcessSensor(MqttPublisher publisher, int? updateInterval = null, string name = "MicrophoneProcess", Guid id = default) : base(publisher, name ?? "MicrophoneProcess", updateInterval ?? 10, id) + { + } + + public override string GetState() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return IsMicrophoneInUseRegistry(); + } + else + { + return "unsupported"; + } + } + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() + { + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() + { + Name = this.Name, + NamePrefix = Publisher.NamePrefix, + Unique_id = this.Id.ToString(), + Device = this.Publisher.DeviceConfigModel, + State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state", + Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability" + }); + } + + [SupportedOSPlatform("windows")] + private void CheckLastUsed(RegistryKey key) + { + foreach (var subKeyName in key.GetSubKeyNames()) + { + // NonPackaged has multiple subkeys + if (subKeyName == "NonPackaged") + { + using (var nonpackagedkey = key.OpenSubKey(subKeyName)) + { + CheckLastUsed(nonpackagedkey); + } + } + else + { + using (var subKey = key.OpenSubKey(subKeyName)) + { + if (subKey.GetValueNames().Contains("LastUsedTimeStop")) + { + var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; + if (endTime <= 0) + { + this.processes.Add(subKeyName); + } + } + } + } + } + } + + [SupportedOSPlatform("windows")] + private string IsMicrophoneInUseRegistry() + { + // Clear old values + this.processes.Clear(); + + using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone")) + { + CheckLastUsed(key); + } + + using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone")) + { + CheckLastUsed(key); + } + + if (this.processes.Count() > 0) + { + return String.Join(",", this.processes.ToArray()); + } + return "off"; + } + } +} diff --git a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs index a0b6984..c1601cf 100644 --- a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs +++ b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs @@ -12,13 +12,26 @@ namespace hass_workstation_service.Domain.Sensors public class WMIQuerySensor : AbstractSensor { public string Query { get; private set; } + public string Scope { get; private set; } protected readonly ObjectQuery _objectQuery; protected readonly ManagementObjectSearcher _searcher; - public WMIQuerySensor(MqttPublisher publisher, string query, int? updateInterval = null, string name = "WMIQuerySensor", Guid id = default) : base(publisher, name ?? "WMIQuerySensor", updateInterval ?? 10, id) + public WMIQuerySensor(MqttPublisher publisher, string query, int? updateInterval = null, string name = "WMIQuerySensor", Guid id = default, string scope = "") : base(publisher, name ?? "WMIQuerySensor", updateInterval ?? 10, id) { this.Query = query; + this.Scope = scope; _objectQuery = new ObjectQuery(this.Query); - _searcher = new ManagementObjectSearcher(query); + ManagementScope managementscope; + // if we have a custom scope, use that + if (!string.IsNullOrWhiteSpace(scope)) + { + managementscope = new ManagementScope(scope); + } + // otherwise, use the default + else + { + managementscope = new ManagementScope(@"\\localhost\"); + } + _searcher = new ManagementObjectSearcher(managementscope, _objectQuery); } public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { diff --git a/hass-workstation-service/Domain/Sensors/WebcamProcessSensor.cs b/hass-workstation-service/Domain/Sensors/WebcamProcessSensor.cs index 9210654..4e5ebdf 100644 --- a/hass-workstation-service/Domain/Sensors/WebcamProcessSensor.cs +++ b/hass-workstation-service/Domain/Sensors/WebcamProcessSensor.cs @@ -4,15 +4,18 @@ using System; using System.Linq; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Collections.Generic; namespace hass_workstation_service.Domain.Sensors { public class WebcamProcessSensor : AbstractSensor { + private HashSet processes = new HashSet(); + public WebcamProcessSensor(MqttPublisher publisher, int? updateInterval = null, string name = "WebcamProcess", Guid id = default) : base(publisher, name ?? "WebcamProcess", updateInterval ?? 10, id) { } - + public override string GetState() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) @@ -38,92 +41,55 @@ namespace hass_workstation_service.Domain.Sensors } [SupportedOSPlatform("windows")] - private string IsWebCamInUseRegistry() + private void CheckLastUsed(RegistryKey key) { - using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam")) + foreach (var subKeyName in key.GetSubKeyNames()) { - foreach (var subKeyName in key.GetSubKeyNames()) + // NonPackaged has multiple subkeys + if (subKeyName == "NonPackaged") { - // NonPackaged has multiple subkeys - if (subKeyName == "NonPackaged") + using (var nonpackagedkey = key.OpenSubKey(subKeyName)) { - using (var nonpackagedkey = key.OpenSubKey(subKeyName)) - { - foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames()) - { - using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName)) - { - if (subKey.GetValueNames().Contains("LastUsedTimeStop")) - { - var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; - if (endTime <= 0) - { - return nonpackagedSubKeyName; - } - } - } - } - } + CheckLastUsed(nonpackagedkey); } - else + } + else + { + using (var subKey = key.OpenSubKey(subKeyName)) { - using (var subKey = key.OpenSubKey(subKeyName)) + if (subKey.GetValueNames().Contains("LastUsedTimeStop")) { - if (subKey.GetValueNames().Contains("LastUsedTimeStop")) + var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; + if (endTime <= 0) { - var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; - if (endTime <= 0) - { - return subKeyName; - } + this.processes.Add(subKeyName); } } } } } + } + + [SupportedOSPlatform("windows")] + private string IsWebCamInUseRegistry() + { + // Clear old values + this.processes.Clear(); + + using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam")) + { + CheckLastUsed(key); + } using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam")) { - foreach (var subKeyName in key.GetSubKeyNames()) - { - // NonPackaged has multiple subkeys - if (subKeyName == "NonPackaged") - { - using (var nonpackagedkey = key.OpenSubKey(subKeyName)) - { - foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames()) - { - using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName)) - { - if (subKey.GetValueNames().Contains("LastUsedTimeStop")) - { - var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; - if (endTime <= 0) - { - return nonpackagedSubKeyName; - } - } - } - } - } - } - else - { - using (var subKey = key.OpenSubKey(subKeyName)) - { - if (subKey.GetValueNames().Contains("LastUsedTimeStop")) - { - var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; - if (endTime <= 0) - { - return subKeyName; - } - } - } - } - } + CheckLastUsed(key); } + if (this.processes.Count() > 0) + { + return String.Join(",", this.processes.ToArray()); + } return "off"; } } diff --git a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml b/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml index cb38d89..6af45d4 100644 --- a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml +++ b/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml @@ -4,11 +4,12 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - 57 + 58 1.0.0.* True Release True + True https://github.com/sleevezipper/hass-workstation-service True true @@ -16,8 +17,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121. https://hassworkstationstorage.z6.web.core.windows.net/publish/ True True - 820B7EDF3E26E24BB4C25B177A05B3D0C77BF73A - hass-workstation-service_TemporaryKey.pfx + 66F1CEE175B90B89AFD3AA8F5F649268BF4DFA9E + hass-workstation-service_1_TemporaryKey.pfx true false Any CPU @@ -37,12 +38,13 @@ https://go.microsoft.com/fwlink/?LinkID=208121. True True Foreground - False|2021-11-14T15:44:38.1032015Z; + False + Publish.html - true - .NET Runtime 5.0.1 (x64) + True + .NET Runtime 5.0.12 (x64) \ No newline at end of file diff --git a/hass-workstation-service/hass-workstation-service.csproj b/hass-workstation-service/hass-workstation-service.csproj index 30c7c44..0361625 100644 --- a/hass-workstation-service/hass-workstation-service.csproj +++ b/hass-workstation-service/hass-workstation-service.csproj @@ -43,7 +43,7 @@ Always - Always + Never @@ -55,15 +55,15 @@ - - + + - - + + - + - + diff --git a/hass-workstation-service/hass-workstation-service_1_TemporaryKey.pfx b/hass-workstation-service/hass-workstation-service_1_TemporaryKey.pfx new file mode 100644 index 0000000..7c4bb5d Binary files /dev/null and b/hass-workstation-service/hass-workstation-service_1_TemporaryKey.pfx differ