diff --git a/.gitignore b/.gitignore index 790402c..ccdc8b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vs/ + +*.user diff --git a/README.md b/README.md index 76ed755..337ebc6 100644 --- a/README.md +++ b/README.md @@ -7,32 +7,36 @@ This goal of this project is to provide useful sensors and services from your wo - Using well defined standards - Being local when you want it to, only communicating through your own MQTT broker - Being easy to configure +- Using secure communication It will try to futher accomplish this goal in the future by: - Being platform independent -- Using secure communication ## Screenshots -![The settings screen](https://i.imgur.com/KXKQqMr.png) +![The settings screen](https://i.imgur.com/WpCZaDR.png) -![The resulting sensors in Home Assistant](https://i.imgur.com/1Yvx2Ea.png) +![The resulting sensors in Home Assistant](https://i.imgur.com/Kka8VOi.png) ## Installation -You can get the installer from [here](https://hassworkstationstorage.z6.web.core.windows.net/publish/setup.exe). When using the installer, the application checks for updates on startup. +You can get the installer from [here](https://hassworkstationstorage.z6.web.core.windows.net/publish/setup.exe). When using the installer, the application checks for updates on startup. This is the recommended way to install for most users. Note: You'll get a Windows Smartscreen warning because the code was self signed. You can click "More info" and then "Run anyway" to proceed with installing. -If you don't want to use the installer, you can find releases on GitHub [here](https://github.com/sleevezipper/hass-workstation-service/releases). `hass-workstation-service.exe` is the background service and you can use `UserInterface.exe` to configure the service. If you don't use the installer autostart won't work. +### Standalone + +You'll need [.NET 5 Runtime](https://dotnet.microsoft.com/download/dotnet/current/runtime) installed. + +If you don't want to use the installer, you can find the standalone version releases on GitHub [here](https://github.com/sleevezipper/hass-workstation-service/releases). Unpack all files to a folder and run `hass-workstation-service.exe`. This is the background service and you can use `UserInterface.exe` to configure the service. There is no automatic (or prompted) updating in the standalone version. ### Updating -The app checks for updates on startup. If an update is available you will be prompted to install. +If you used the installer, the app checks for updates on startup. If an update is available you will be prompted to install. If you use the standalone, just delete all files from the previous install and unpack the zip to the same location as before. ## Sensors -The application provides several sensors. Sensors can be configured with a name and this name will be used in the MQTT topic like this: `homeassistant/sensor/{Name}/state`. Sensors will expose themselves through [MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) and will automatically appear in Home assistant or any other platform that supports this type of configuration. +The application provides several sensors. Sensors can be configured with a name and this name will be used in the MQTT topic like this: `homeassistant/sensor/{DeviceName}/{Name}/state`. Sensors will expose themselves through [MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/) and will automatically appear in Home assistant or any other platform that supports this type of configuration. Sensors publish their state on their own interval which you can configure and only publish when the state changes. @@ -54,6 +58,14 @@ This sensor watches the UserNotificationState. This is normally used in applicat This sensor exposes the name of the currently focused window. +### WebcamActive + +This sensor shows if the webcam is currently being used. It uses the Windows registry to check will work from Windows 10 version 1903 and higher. + +### MicrophoneActive + +This sensor shows if the microphone is currently being used. It uses the Windows registry to check and will work from Windows 10 version 1903 and higher. + ### CPULoad This sensor checks the current CPU load. It averages the load on all logical cores every second and rounds the output to two decimals. @@ -90,6 +102,25 @@ which results in `4008` for my PC. You can use [WMI Explorer](https://github.com/vinaypamnani/wmie2/tree/v2.0.0.2) to find see what data is available. +### IdleTime + +This sensor returns the amount of seconds the workstation has been idle for. It starts counting the moment you stop typing or moving your mouse. + +### UpTime + +This sensor returns theup time from Windows in seconds. + +### SessionState + +This sensor returns the current session state. It has the following possible states: + +|State|Explanation| +|---|---| +|Locked|All user sessions are locked.| +|LoggedOff|No users are logged in.| +|InUse|A user is currently logged in.| +|Unknown|Something went wrong while getting the status.| + ### Dummy This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it. diff --git a/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml b/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml index 6642b42..7e48eac 100644 --- a/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml +++ b/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml @@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. Any CPU ..\hass-workstation-service\ FileSystem - netcoreapp3.1 + net5.0 win-x64 false True diff --git a/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml.user b/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml.user deleted file mode 100644 index 312c6e3..0000000 --- a/UserInterface/Properties/PublishProfiles/FolderProfile.pubxml.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/UserInterface/Properties/PublishProfiles/Standalone.pubxml b/UserInterface/Properties/PublishProfiles/Standalone.pubxml new file mode 100644 index 0000000..abe2514 --- /dev/null +++ b/UserInterface/Properties/PublishProfiles/Standalone.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Userinterface-standalone + FileSystem + net5.0 + win-x64 + true + False + False + False + + \ No newline at end of file diff --git a/UserInterface/UserInterface.csproj b/UserInterface/UserInterface.csproj index 3938707..c2877e3 100644 --- a/UserInterface/UserInterface.csproj +++ b/UserInterface/UserInterface.csproj @@ -1,7 +1,7 @@  WinExe - netcoreapp3.1 + net5.0 @@ -17,6 +17,7 @@ + diff --git a/UserInterface/UserInterface.csproj.user b/UserInterface/UserInterface.csproj.user deleted file mode 100644 index 3d9bdf3..0000000 --- a/UserInterface/UserInterface.csproj.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - <_LastSelectedProfileId>C:\Users\Maurits\Documents\Repo\hass-desktop-service\UserInterface\Properties\PublishProfiles\FolderProfile.pubxml - - \ No newline at end of file diff --git a/UserInterface/ViewModels/AddSensorViewModel.cs b/UserInterface/ViewModels/AddSensorViewModel.cs index 665e4ad..e3303f6 100644 --- a/UserInterface/ViewModels/AddSensorViewModel.cs +++ b/UserInterface/ViewModels/AddSensorViewModel.cs @@ -16,9 +16,12 @@ namespace UserInterface.ViewModels public bool ShowQueryInput { get => showQueryInput; set => this.RaiseAndSetIfChanged(ref showQueryInput, value); } public bool ShowWindowNameInput { get => showWindowNameInput; set => this.RaiseAndSetIfChanged(ref showWindowNameInput, value); } + public bool ShowDetectionModeOptions { get => showDetectionModeOptions; set => this.RaiseAndSetIfChanged(ref showDetectionModeOptions, value); } + private string moreInfoLink; private int updateInterval; private bool showWindowNameInput; + private bool showDetectionModeOptions; public string MoreInfoLink { @@ -28,6 +31,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; } diff --git a/UserInterface/ViewModels/BrokerSettingsViewModel.cs b/UserInterface/ViewModels/BrokerSettingsViewModel.cs index 42e8ea6..2811cb3 100644 --- a/UserInterface/ViewModels/BrokerSettingsViewModel.cs +++ b/UserInterface/ViewModels/BrokerSettingsViewModel.cs @@ -2,6 +2,7 @@ using ReactiveUI; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Text; namespace UserInterface.ViewModels @@ -13,17 +14,28 @@ namespace UserInterface.ViewModels private string password; private string message; private bool isConnected; + private int? port; + private bool useTLS; public bool IsConnected { get => isConnected; set => this.RaiseAndSetIfChanged(ref isConnected, value); } public string Message { get => message; set => this.RaiseAndSetIfChanged(ref message, value); } + [Required(AllowEmptyStrings = false)] public string Host { get => host; set => this.RaiseAndSetIfChanged(ref host, value); } public string Username { get => username; set => this.RaiseAndSetIfChanged(ref username, value); } public string Password { get => password; set => this.RaiseAndSetIfChanged(ref password, value); } + [Required] + [Range(1, 65535)] + public int? Port { get => port; set => this.RaiseAndSetIfChanged(ref port, value); } + public bool UseTLS { get => useTLS; set => this.RaiseAndSetIfChanged(ref useTLS, value); } + + public void Update(MqttSettings settings) { this.Host = settings.Host; this.Username = settings.Username; this.Password = settings.Password; + this.Port = settings.Port; + this.UseTLS = settings.UseTLS; } public void UpdateStatus(MqqtClientStatus status) diff --git a/UserInterface/ViewModels/SensorSettingsViewModel.cs b/UserInterface/ViewModels/SensorSettingsViewModel.cs index 0e4e9f4..1939bf6 100644 --- a/UserInterface/ViewModels/SensorSettingsViewModel.cs +++ b/UserInterface/ViewModels/SensorSettingsViewModel.cs @@ -40,7 +40,7 @@ namespace UserInterface.ViewModels { if (!string.IsNullOrWhiteSpace(_value)) { - return _value + UnitOfMeasurement; + return _value + " " + UnitOfMeasurement; } else return ""; diff --git a/UserInterface/ViewModels/ViewModelBase.cs b/UserInterface/ViewModels/ViewModelBase.cs index 987b7c8..5885880 100644 --- a/UserInterface/ViewModels/ViewModelBase.cs +++ b/UserInterface/ViewModels/ViewModelBase.cs @@ -1,11 +1,18 @@ using ReactiveUI; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Text; namespace UserInterface.ViewModels { public class ViewModelBase : ReactiveObject { + public bool IsValid(T obj, out ICollection results) + { + results = new List(); + + return Validator.TryValidateObject(obj, new ValidationContext(obj), results, true); + } } } diff --git a/UserInterface/Views/AddSensorDialog.axaml b/UserInterface/Views/AddSensorDialog.axaml index e80f016..10d62ec 100644 --- a/UserInterface/Views/AddSensorDialog.axaml +++ b/UserInterface/Views/AddSensorDialog.axaml @@ -26,6 +26,6 @@ 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 ba83103..3cd05a7 100644 --- a/UserInterface/Views/AddSensorDialog.axaml.cs +++ b/UserInterface/Views/AddSensorDialog.axaml.cs @@ -19,14 +19,17 @@ namespace UserInterface.Views { private readonly IIpcClient client; public ComboBox comboBox { get; set; } + public ComboBox detectionModecomboBox { get; set; } public AddSensorDialog() { this.InitializeComponent(); #if DEBUG this.AttachDevTools(); #endif + DataContext = new AddSensorViewModel(); this.comboBox = this.FindControl("ComboBox"); - this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast(); + this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast().OrderBy(v => v.ToString()); + this.comboBox.SelectedIndex = 0; // register IPC clients ServiceProvider serviceProvider = new ServiceCollection() @@ -39,15 +42,12 @@ namespace UserInterface.Views // create client this.client = clientFactory.CreateClient("addsensor"); - - - DataContext = new AddSensorViewModel(); } public async void Save(object sender, RoutedEventArgs args) { var item = ((AddSensorViewModel)this.DataContext); - dynamic model = new { item.Name, item.Query, item.UpdateInterval, item.WindowName }; + 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(); @@ -61,6 +61,7 @@ namespace UserInterface.Views case AvailableSensors.UserNotificationStateSensor: 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.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.ShowWindowNameInput = false; item.UpdateInterval = 5; @@ -68,6 +69,7 @@ namespace UserInterface.Views 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.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.ShowWindowNameInput = false; item.UpdateInterval = 1; @@ -75,6 +77,7 @@ namespace UserInterface.Views 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.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.ShowWindowNameInput = false; item.UpdateInterval = 5; @@ -82,6 +85,7 @@ namespace UserInterface.Views 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.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.ShowWindowNameInput = false; item.UpdateInterval = 3600; @@ -89,13 +93,15 @@ namespace UserInterface.Views 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.ShowDetectionModeOptions = false; 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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#usedmemory"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.ShowWindowNameInput = false; item.UpdateInterval = 10; @@ -103,10 +109,25 @@ namespace UserInterface.Views 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.ShowDetectionModeOptions = false; item.ShowQueryInput = false; 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#webcamactive"; + item.ShowDetectionModeOptions = true; + 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#microphoneactive"; + item.ShowDetectionModeOptions = false; + 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#namedwindow"; @@ -114,6 +135,27 @@ namespace UserInterface.Views item.ShowWindowNameInput = true; item.UpdateInterval = 5; break; + case AvailableSensors.IdleTimeSensor: + item.Description = "This sensor returns the amount of seconds the workstation has been idle for. "; + item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#idletime"; + item.ShowQueryInput = false; + item.ShowWindowNameInput = false; + item.UpdateInterval = 5; + break; + case AvailableSensors.UpTimeSensor: + item.Description = "This sensor returns the uptime from Windows in seconds"; + item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#uptime"; + item.ShowQueryInput = false; + 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#sessionstate"; + item.ShowQueryInput = false; + 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 dfea2d9..ee3df95 100644 --- a/UserInterface/Views/BrokerSettings.axaml +++ b/UserInterface/Views/BrokerSettings.axaml @@ -10,6 +10,17 @@ IP or hostname + + + Port + + + + + Use TLS + + + Username Password diff --git a/UserInterface/Views/BrokerSettings.axaml.cs b/UserInterface/Views/BrokerSettings.axaml.cs index 0862a86..3e59837 100644 --- a/UserInterface/Views/BrokerSettings.axaml.cs +++ b/UserInterface/Views/BrokerSettings.axaml.cs @@ -10,6 +10,8 @@ using System.Reactive.Linq; using UserInterface.ViewModels; using System.Security; using hass_workstation_service.Communication.InterProcesCommunication.Models; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; namespace UserInterface.Views { @@ -45,7 +47,11 @@ namespace UserInterface.Views public void Configure(object sender, RoutedEventArgs args) { var model = (BrokerSettingsViewModel)this.DataContext; - var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "" })); + 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 })); + } } public async void GetSettings() diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs index dea8726..51e9313 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs @@ -1,4 +1,4 @@ -using hass_workstation_service.Communication.InterProcesCommunication.Models; +using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.NamedPipe; using hass_workstation_service.Communication.Util; using hass_workstation_service.Data; @@ -106,9 +106,24 @@ namespace hass_workstation_service.Communication.InterProcesCommunication case AvailableSensors.ActiveWindowSensor: sensorToCreate = new ActiveWindowSensor(this._publisher, (int)model.UpdateInterval, model.Name); break; + case AvailableSensors.WebcamActiveSensor: + sensorToCreate = new WebcamActiveSensor(this._publisher, (int)model.UpdateInterval, model.Name); + break; + case AvailableSensors.MicrophoneActiveSensor: + sensorToCreate = new MicrophoneActiveSensor(this._publisher, (int)model.UpdateInterval, model.Name); + break; case AvailableSensors.NamedWindowSensor: sensorToCreate = new NamedWindowSensor(this._publisher, model.WindowName, model.Name, (int)model.UpdateInterval); break; + case AvailableSensors.IdleTimeSensor: + sensorToCreate = new IdleTimeSensor(this._publisher,(int)model.UpdateInterval, model.Name); + break; + case AvailableSensors.UpTimeSensor: + sensorToCreate = new UpTimeSensor(this._publisher, (int)model.UpdateInterval, model.Name); + break; + case AvailableSensors.SessionStateSensor: + sensorToCreate = new SessionStateSensor(this._publisher, (int)model.UpdateInterval, model.Name); + 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 9fc20e5..e7fa359 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs @@ -1,4 +1,4 @@ -using hass_workstation_service.Domain.Sensors; +using hass_workstation_service.Domain.Sensors; using System; using System.Collections.Generic; using System.Text; @@ -10,6 +10,8 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models public string Host { get; set; } public string Username { get; set; } public string Password { get; set; } + public int? Port { get; set; } + public bool UseTLS { get; set; } } public class MqqtClientStatus @@ -36,7 +38,12 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models CPULoadSensor, WMIQuerySensor, MemoryUsageSensor, + WebcamActiveSensor, + MicrophoneActiveSensor, ActiveWindowSensor, - NamedWindowSensor + NamedWindowSensor, + IdleTimeSensor, + UpTimeSensor, + SessionStateSensor } } diff --git a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs index 163566c..090cb2e 100644 --- a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs +++ b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs @@ -10,6 +10,7 @@ using MQTTnet; using MQTTnet.Adapter; using MQTTnet.Client; using MQTTnet.Client.Options; +using MQTTnet.Exceptions; using Serilog; namespace hass_workstation_service.Communication @@ -113,7 +114,7 @@ namespace hass_workstation_service.Communication PropertyNameCaseInsensitive = true }; var message = new MqttApplicationMessageBuilder() - .WithTopic($"homeassistant/sensor/{config.Name}/config") + .WithTopic($"homeassistant/sensor/{this.DeviceConfigModel.Name}/{config.Name}/config") .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, options)) .WithRetainFlag() .Build(); @@ -135,7 +136,11 @@ namespace hass_workstation_service.Communication this._mqttClientMessage = ex.ResultCode.ToString(); Log.Logger.Error("Could not connect to broker: " + ex.ResultCode.ToString()); } - + catch (MqttCommunicationException ex) + { + this._mqttClientMessage = ex.ToString(); + Log.Logger.Error("Could not connect to broker: " + ex.Message); + } } public MqqtClientStatus GetStatus() diff --git a/hass-workstation-service/Data/ConfigurationService.cs b/hass-workstation-service/Data/ConfigurationService.cs index d46bd6c..3adbef2 100644 --- a/hass-workstation-service/Data/ConfigurationService.cs +++ b/hass-workstation-service/Data/ConfigurationService.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Security; using System.Text.Json; using System.Threading.Tasks; @@ -23,8 +25,8 @@ namespace hass_workstation_service.Data public ICollection ConfiguredSensors { get; private set; } public Action MqqtConfigChangedHandler { get; set; } - public bool _brokerSettingsFileLocked { get; set; } - public bool _sensorsSettingsFileLocked { get; set; } + private bool BrokerSettingsFileLocked { get; set; } + private bool SensorsSettingsFileLocked { get; set; } private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service"); @@ -32,12 +34,12 @@ namespace hass_workstation_service.Data { if (!File.Exists(Path.Combine(path, "mqttbroker.json"))) { - File.Create(Path.Combine(path, "mqttbroker.json")); + File.Create(Path.Combine(path, "mqttbroker.json")).Close(); } if (!File.Exists(Path.Combine(path, "configured-sensors.json"))) { - File.Create(Path.Combine(path, "configured-sensors.json")); + File.Create(Path.Combine(path, "configured-sensors.json")).Close(); } ConfiguredSensors = new List(); @@ -45,11 +47,11 @@ namespace hass_workstation_service.Data public async void ReadSensorSettings(MqttPublisher publisher) { - while (this._sensorsSettingsFileLocked) + while (this.SensorsSettingsFileLocked) { await Task.Delay(500); } - this._sensorsSettingsFileLocked = true; + this.SensorsSettingsFileLocked = true; List sensors = new List(); using (var stream = new FileStream(Path.Combine(path, "configured-sensors.json"), FileMode.Open)) { @@ -59,7 +61,7 @@ namespace hass_workstation_service.Data sensors = await JsonSerializer.DeserializeAsync>(stream); } stream.Close(); - this._sensorsSettingsFileLocked = false; + this.SensorsSettingsFileLocked = false; } foreach (ConfiguredSensor configuredSensor in sensors) @@ -76,14 +78,14 @@ namespace hass_workstation_service.Data case "CurrentClockSpeedSensor": sensor = new CurrentClockSpeedSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; - case "WMIQuerySensor": - sensor = new WMIQuerySensor(publisher, configuredSensor.Query, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); - break; case "CPULoadSensor": sensor = new CPULoadSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; case "MemoryUsageSensor": - sensor = new MemoryUsageSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + sensor = new MemoryUsageSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + } break; case "ActiveWindowSensor": sensor = new ActiveWindowSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); @@ -91,6 +93,25 @@ namespace hass_workstation_service.Data case "NamedWindowSensor": sensor = new NamedWindowSensor(publisher, configuredSensor.WindowName, configuredSensor.Name, configuredSensor.UpdateInterval, configuredSensor.Id); break; + case "IdleTimeSensor": + sensor = new IdleTimeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; + case "UpTimeSensor": + sensor = new UpTimeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; + case "WebcamActiveSensor": + sensor = new WebcamActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; + case "MicrophoneActiveSensor": + sensor = new MicrophoneActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; + case "SessionStateSensor": + sensor = new SessionStateSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; + // keep this one last! + case "WMIQuerySensor": + sensor = new WMIQuerySensor(publisher, configuredSensor.Query, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + break; default: Log.Logger.Error("unsupported sensor type in config"); break; @@ -109,8 +130,12 @@ namespace hass_workstation_service.Data { var mqttClientOptions = new MqttClientOptionsBuilder() - .WithTcpServer(configuredBroker.Host) - // .WithTls() + .WithTcpServer(configuredBroker.Host, configuredBroker.Port) + .WithTls(new MqttClientOptionsBuilderTlsParameters() + { + UseTls = configuredBroker.UseTLS, + AllowUntrustedCertificates = true + }) .WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString()) .Build(); return mqttClientOptions; @@ -128,11 +153,11 @@ namespace hass_workstation_service.Data /// public async Task ReadMqttSettingsAsync() { - while (this._brokerSettingsFileLocked) + while (this.BrokerSettingsFileLocked) { await Task.Delay(500); } - this._brokerSettingsFileLocked = true; + this.BrokerSettingsFileLocked = true; ConfiguredMqttBroker configuredBroker = null; using (FileStream stream = new FileStream(Path.Combine(path, "mqttbroker.json"), FileMode.Open)) { @@ -144,17 +169,17 @@ namespace hass_workstation_service.Data stream.Close(); } - this._brokerSettingsFileLocked = false; + this.BrokerSettingsFileLocked = false; return configuredBroker; } public async void WriteSettingsAsync() { - while (this._sensorsSettingsFileLocked) + while (this.SensorsSettingsFileLocked) { await Task.Delay(500); } - this._sensorsSettingsFileLocked = true; + this.SensorsSettingsFileLocked = true; List configuredSensorsToSave = new List(); using (FileStream stream = new FileStream(Path.Combine(path, "configured-sensors.json"), FileMode.Open)) { @@ -162,14 +187,14 @@ namespace hass_workstation_service.Data Log.Logger.Information($"writing configured sensors to: {stream.Name}"); foreach (AbstractSensor sensor in this.ConfiguredSensors) { - if (sensor is WMIQuerySensor) + if (sensor is WMIQuerySensor wmiSensor) { - var wmiSensor = (WMIQuerySensor)sensor; +#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 }); +#pragma warning restore CA1416 // Validate platform compatibility } - if (sensor is NamedWindowSensor) + else if (sensor is NamedWindowSensor 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 @@ -182,7 +207,7 @@ namespace hass_workstation_service.Data await JsonSerializer.SerializeAsync(stream, configuredSensorsToSave); stream.Close(); } - this._sensorsSettingsFileLocked = false; + this.SensorsSettingsFileLocked = false; } public void AddConfiguredSensor(AbstractSensor sensor) @@ -220,11 +245,11 @@ namespace hass_workstation_service.Data /// public async void WriteMqttBrokerSettingsAsync(MqttSettings settings) { - while (this._brokerSettingsFileLocked) + while (this.BrokerSettingsFileLocked) { await Task.Delay(500); } - this._brokerSettingsFileLocked = true; + this.BrokerSettingsFileLocked = true; using (FileStream stream = new FileStream(Path.Combine(path, "mqttbroker.json"), FileMode.Open)) { stream.SetLength(0); @@ -234,13 +259,15 @@ namespace hass_workstation_service.Data { Host = settings.Host, Username = settings.Username, - Password = settings.Password ?? "" + Password = settings.Password ?? "", + Port = settings.Port ?? 1883, + UseTLS = settings.UseTLS }; await JsonSerializer.SerializeAsync(stream, configuredBroker); stream.Close(); } - this._brokerSettingsFileLocked = false; + this.BrokerSettingsFileLocked = false; this.MqqtConfigChangedHandler.Invoke(await this.GetMqttClientOptionsAsync()); } @@ -251,7 +278,9 @@ namespace hass_workstation_service.Data { Host = broker?.Host, Username = broker?.Username, - Password = broker?.Password + Password = broker?.Password, + Port = broker?.Port, + UseTLS = broker?.UseTLS ?? false }; } @@ -259,6 +288,7 @@ namespace hass_workstation_service.Data /// Enable or disable autostarting the background service. It does this by adding the application shortcut (appref-ms) to the registry run key for the current user /// /// + [SupportedOSPlatform("windows")] public void EnableAutoStart(bool enable) { if (enable) @@ -267,8 +297,22 @@ namespace hass_workstation_service.Data // The path to the key where Windows looks for startup applications RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true); - //Path to launch shortcut - string startPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs) + @"\Sleevezipper\Hass Workstation Service.appref-ms"; + Log.Information("currentDir: " + Environment.CurrentDirectory); + Log.Information("appData: " + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)); + + string startPath; + // if the app is installed in appdata, we can assume it was installed using the installer + if (Environment.CurrentDirectory.Contains(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData))) + { + // so we set the autostart Path to launch shortcut + startPath = Environment.GetFolderPath(Environment.SpecialFolder.Programs) + @"\Sleevezipper\Hass Workstation Service.appref-ms"; + } + else + { + // if it isn't in appdata, it's probably running as standalone and we set the startpath to the path of the executable + startPath = Environment.CurrentDirectory + @"\hass-workstation-service.exe"; + } + rkApp.SetValue("hass-workstation-service", startPath); rkApp.Close(); @@ -282,6 +326,7 @@ namespace hass_workstation_service.Data } } + [SupportedOSPlatform("windows")] public bool IsAutoStartEnabled() { RegistryKey rkApp = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true); diff --git a/hass-workstation-service/Data/ConfiguredMqttBroker.cs b/hass-workstation-service/Data/ConfiguredMqttBroker.cs index a86e249..f188322 100644 --- a/hass-workstation-service/Data/ConfiguredMqttBroker.cs +++ b/hass-workstation-service/Data/ConfiguredMqttBroker.cs @@ -5,8 +5,35 @@ namespace hass_workstation_service.Data { public class ConfiguredMqttBroker { + private string username; + private string password; + private int? port; + public string Host { get; set; } - public string Username { get; set; } - public string Password { get; set; } + public int Port { get => port ?? 1883; set => port = value; } + public bool UseTLS { get; set; } + + + public string Username + { + get + { + if (username != null) return username; + + return ""; + } + set => username = value; + } + + public string Password + { + get + { + if (password != null) return password; + + return ""; + } + set => password = value; + } } } \ No newline at end of file diff --git a/hass-workstation-service/Data/ConfiguredSensor.cs b/hass-workstation-service/Data/ConfiguredSensor.cs index fd629dd..dcb29eb 100644 --- a/hass-workstation-service/Data/ConfiguredSensor.cs +++ b/hass-workstation-service/Data/ConfiguredSensor.cs @@ -1,3 +1,4 @@ +using hass_workstation_service.Domain.Sensors; using System; namespace hass_workstation_service.Data diff --git a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs index ffd1551..1b095bc 100644 --- a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs +++ b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs @@ -19,7 +19,7 @@ namespace hass_workstation_service.Domain.Sensors public MqttPublisher Publisher { get; protected set; } public AbstractSensor(MqttPublisher publisher, string name, int updateInterval = 10, Guid id = default(Guid)) { - if (id == Guid.Empty || id == null) + if (id == Guid.Empty) { this.Id = Guid.NewGuid(); } diff --git a/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs b/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs index 1f13278..d431f14 100644 --- a/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs +++ b/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs @@ -16,7 +16,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:window-maximize", }); } diff --git a/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs b/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs index 2b8b31e..7000943 100644 --- a/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs +++ b/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs @@ -4,10 +4,13 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Management; +using System.Runtime.Versioning; using System.Text; namespace hass_workstation_service.Domain.Sensors { + + [SupportedOSPlatform("windows")] public class CPULoadSensor : WMIQuerySensor { public CPULoadSensor(MqttPublisher publisher, int? updateInterval = null, string name = "CPULoadSensor", Guid id = default) : base(publisher, "SELECT PercentProcessorTime FROM Win32_PerfFormattedData_PerfOS_Processor", updateInterval ?? 10, name ?? "CPULoadSensor", id) @@ -21,11 +24,13 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:chart-areaspline", Unit_of_measurement = "%" }); } + + [SupportedOSPlatform("windows")] public override string GetState() { ManagementObjectCollection collection = _searcher.Get(); diff --git a/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs b/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs index d1ced62..921a96d 100644 --- a/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs +++ b/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs @@ -16,7 +16,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:speedometer", Unit_of_measurement = "MHz" }); diff --git a/hass-workstation-service/Domain/Sensors/DummySensor.cs b/hass-workstation-service/Domain/Sensors/DummySensor.cs index b887338..5960104 100644 --- a/hass-workstation-service/Domain/Sensors/DummySensor.cs +++ b/hass-workstation-service/Domain/Sensors/DummySensor.cs @@ -20,7 +20,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state" + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state" }); } diff --git a/hass-workstation-service/Domain/Sensors/IdleTimeSensor.cs b/hass-workstation-service/Domain/Sensors/IdleTimeSensor.cs new file mode 100644 index 0000000..5d69032 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/IdleTimeSensor.cs @@ -0,0 +1,68 @@ +using hass_workstation_service.Communication; +using System; +using System.Runtime.InteropServices; + +namespace hass_workstation_service.Domain.Sensors +{ + public class IdleTimeSensor : AbstractSensor + { + + public IdleTimeSensor(MqttPublisher publisher, int? updateInterval = 10, string name = "IdleTime", Guid id = default) : base(publisher, name ?? "IdleTime", updateInterval ?? 10, id){} + + 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/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:clock-time-three-outline", + Unit_of_measurement = "seconds" + }); + } + + public override string GetState() + { + return GetLastInputTime().ToString(); + } + + + + + static int GetLastInputTime() + { + int idleTime = 0; + LASTINPUTINFO lastInputInfo = new LASTINPUTINFO(); + lastInputInfo.cbSize = Marshal.SizeOf(lastInputInfo); + lastInputInfo.dwTime = 0; + + int envTicks = Environment.TickCount; + + if (GetLastInputInfo(ref lastInputInfo)) + { + int lastInputTick = Convert.ToInt32(lastInputInfo.dwTime); + + idleTime = envTicks - lastInputTick; + } + + return ((idleTime > 0) ? (idleTime / 1000) : idleTime); + } + + + + [DllImport("User32.dll")] + private static extern bool GetLastInputInfo(ref LASTINPUTINFO plii); + + [StructLayout(LayoutKind.Sequential)] + struct LASTINPUTINFO + { + public static readonly int SizeOf = Marshal.SizeOf(typeof(LASTINPUTINFO)); + + [MarshalAs(UnmanagedType.U4)] + public int cbSize; + [MarshalAs(UnmanagedType.U4)] + public UInt32 dwTime; + } + } +} diff --git a/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs b/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs index 75ebf38..d905475 100644 --- a/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs +++ b/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs @@ -3,13 +3,16 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Management; +using System.Runtime.Versioning; using System.Text; namespace hass_workstation_service.Domain.Sensors { + + [SupportedOSPlatform("windows")] public class MemoryUsageSensor : WMIQuerySensor { - public MemoryUsageSensor(MqttPublisher publisher, int? updateInterval = null, string name = "WMIQuerySensor", Guid id = default) : base(publisher, "SELECT FreePhysicalMemory,TotalVisibleMemorySize FROM Win32_OperatingSystem", updateInterval ?? 10, name, id) + public MemoryUsageSensor(MqttPublisher publisher, int? updateInterval = null, string name = "MemoryUsage", Guid id = default) : base(publisher, "SELECT FreePhysicalMemory,TotalVisibleMemorySize FROM Win32_OperatingSystem", updateInterval ?? 10, name ?? "MemoryUsage", id) { } public override string GetState() @@ -38,7 +41,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:memory", Unit_of_measurement = "%" }); diff --git a/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs new file mode 100644 index 0000000..0acf53d --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs @@ -0,0 +1,60 @@ +using hass_workstation_service.Communication; +using Microsoft.Win32; +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace hass_workstation_service.Domain.Sensors +{ + + public class MicrophoneActiveSensor : AbstractSensor + { + public MicrophoneActiveSensor(MqttPublisher publisher, int? updateInterval = null, string name = "MicrophoneActive", Guid id = default(Guid)) : base(publisher, name ?? "MicrophoneActive", updateInterval ?? 10, id) + { + } + public override string GetState() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return IsMicrophoneInUse() ? "True" : "False"; + } + else return "unsupported"; + } + 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/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:microphone", + }); + } + + [SupportedOSPlatform("windows")] + private bool IsMicrophoneInUse() + { + using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged")) + { + foreach (var subKeyName in key.GetSubKeyNames()) + { + 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 true; + } + } + } + } + } + + return false; + } + } +} diff --git a/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs index fe6c620..7cf7989 100644 --- a/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs +++ b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs @@ -23,7 +23,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:window-maximize", }); } diff --git a/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs b/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs new file mode 100644 index 0000000..dc20f38 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs @@ -0,0 +1,101 @@ +using hass_workstation_service.Communication; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace hass_workstation_service.Domain.Sensors +{ + enum PCUserStatuses + { + /// + /// all users are locked + /// + Locked, + /// + /// No users are logged in + /// + LoggedOff, + /// + /// A user is using this computer + /// + InUse, + /// + /// unable to connect to computer / other error + /// + Unknown + } + + public class SessionStateSensor : AbstractSensor + { + public SessionStateSensor(MqttPublisher publisher, int? updateInterval = null, string name = "SessionState", Guid id = default(Guid)) : base(publisher, name ?? "SessionState", updateInterval ?? 10, id) { } + 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/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:lock", + }); + } + + public override string GetState() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return GetPCUserStatus().ToString(); + } + else return "unsupported"; + } + + + [SupportedOSPlatform("windows")] + PCUserStatuses GetPCUserStatus() + { + try + { + + var scope = new ManagementScope(); + scope.Connect(); + + var explorerProcesses = Process.GetProcessesByName("explorer") + .Select(p => p.Id.ToString()) + .ToHashSet(); + + var REprocessid = new Regex(@"(?<=Handle="").*?(?="")", RegexOptions.Compiled); + + var numberOfLogonSessionsWithExplorer = new ManagementObjectSearcher(scope, new SelectQuery("SELECT * FROM Win32_SessionProcess")).Get() + .Cast() + .Where(mo => explorerProcesses.Contains(REprocessid.Match(mo["Dependent"].ToString()).Value)) + .Select(mo => mo["Antecedent"].ToString()) + .Distinct() + .Count(); + + var numberOfUserDesktops = new ManagementObjectSearcher(scope, new SelectQuery("select * from win32_Perfrawdata_TermService_TerminalServicesSession")).Get().Count - 1; // don't count Service desktop + var numberOflogonUIProcesses = Process.GetProcessesByName("LogonUI").Length; + + if (numberOflogonUIProcesses >= numberOfUserDesktops) + { + if (numberOfLogonSessionsWithExplorer > 0) + return PCUserStatuses.Locked; + else + return PCUserStatuses.LoggedOff; + } + else + return PCUserStatuses.InUse; + } + catch + { + return PCUserStatuses.Unknown; + } + } + } +} diff --git a/hass-workstation-service/Domain/Sensors/UpTimeSensor.cs b/hass-workstation-service/Domain/Sensors/UpTimeSensor.cs new file mode 100644 index 0000000..33edabb --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/UpTimeSensor.cs @@ -0,0 +1,42 @@ +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 UpTimeSensor : AbstractSensor + { + + public UpTimeSensor(MqttPublisher publisher, int? updateInterval = 10, string name = "UpTime", Guid id = default) : base(publisher, name ?? "UpTime", updateInterval ?? 10, id) + { + + + } + + 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/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:clock-time-three-outline", + Unit_of_measurement = "seconds" + }); + } + + public override string GetState() + { + + return (GetTickCount64() / 1000).ToString(); //return in seconds + } + + [DllImport("kernel32")] + extern static UInt64 GetTickCount64(); + } +} diff --git a/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs b/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs index 25f0252..e98e1d9 100644 --- a/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs +++ b/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs @@ -16,7 +16,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", Icon = "mdi:laptop", }); } diff --git a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs index 0e9204d..ed44784 100644 --- a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs +++ b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs @@ -2,16 +2,19 @@ using System; using System.Collections.Generic; using System.Management; +using System.Runtime.Versioning; using System.Text; namespace hass_workstation_service.Domain.Sensors { + + [SupportedOSPlatform("windows")] public class WMIQuerySensor : AbstractSensor { public string Query { 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(Guid)) : base(publisher, name ?? "WMIQuerySensor", updateInterval ?? 10, id) + public WMIQuerySensor(MqttPublisher publisher, string query, int? updateInterval = null, string name = "WMIQuerySensor", Guid id = default) : base(publisher, name ?? "WMIQuerySensor", updateInterval ?? 10, id) { this.Query = query; _objectQuery = new ObjectQuery(this.Query); @@ -24,7 +27,7 @@ namespace hass_workstation_service.Domain.Sensors Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{this.Name}/state", + State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", }); } diff --git a/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs new file mode 100644 index 0000000..6806e78 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs @@ -0,0 +1,62 @@ +using hass_workstation_service.Communication; +using Microsoft.Win32; +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace hass_workstation_service.Domain.Sensors +{ + public class WebcamActiveSensor : AbstractSensor + { + public WebcamActiveSensor(MqttPublisher publisher, int? updateInterval = null, string name = "WebcamActive", Guid id = default) : base(publisher, name ?? "WebcamActive", updateInterval ?? 10, id) + { + } + public override string GetState() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return IsWebCamInUseRegistry() ? "True" : "False"; + } + else + { + return "unsupported"; + } + } + 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/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:webcam", + }); + } + + [SupportedOSPlatform("windows")] + private bool IsWebCamInUseRegistry() + { + using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam\NonPackaged")) + { + foreach (var subKeyName in key.GetSubKeyNames()) + { + 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 true; + } + } + } + } + } + + return false; + } + } +} diff --git a/hass-workstation-service/Program.cs b/hass-workstation-service/Program.cs index 3be2fbb..61f597d 100644 --- a/hass-workstation-service/Program.cs +++ b/hass-workstation-service/Program.cs @@ -72,13 +72,14 @@ namespace hass_workstation_service } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .UseSerilog() + .ConfigureLogging((hostContext, loggingBuilder) => + loggingBuilder.AddSerilog(dispose: true)) .ConfigureServices((hostContext, services) => { var deviceConfig = new DeviceConfigModel { Name = Environment.MachineName, - Identifiers = "hass-workstation-service", + Identifiers = "hass-workstation-service" + Environment.MachineName, Manufacturer = Environment.UserName, Model = Environment.OSVersion.ToString(), Sw_version = GetVersion() diff --git a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml b/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml index 35da0ec..558f6d5 100644 --- a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml +++ b/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - 25 + 27 1.0.0.* True Release @@ -33,15 +33,15 @@ https://go.microsoft.com/fwlink/?LinkID=208121. sha256RSA True https://github.com/sleevezipper/hass-workstation-service - netcoreapp3.1 + net5.0 True True Foreground - + true - .NET Core Runtime 3.1.10 (x64) + .NET Runtime 5.0.1 (x64) \ No newline at end of file diff --git a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml.user b/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml.user deleted file mode 100644 index a32fee2..0000000 --- a/hass-workstation-service/Properties/PublishProfiles/AzureHosted.pubxml.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml b/hass-workstation-service/Properties/PublishProfiles/ClickOnceFrameworkDependent.pubxml similarity index 74% rename from hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml rename to hass-workstation-service/Properties/PublishProfiles/ClickOnceFrameworkDependent.pubxml index 23c1c0c..3cb142a 100644 --- a/hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml +++ b/hass-workstation-service/Properties/PublishProfiles/ClickOnceFrameworkDependent.pubxml @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - 1 + 10 1.0.0.* True Release @@ -19,23 +19,25 @@ https://go.microsoft.com/fwlink/?LinkID=208121. true false Any CPU - bin\publish\ - bin\publish\ + bin\clickonce-framework-dependent\ + bin\clickonce-framework-dependent\ ClickOnce - True - True + Sleevezipper + Hass Workstation Service + False + False win-x64 False sha256RSA True - netcoreapp3.1 + net5.0 False Foreground - + true - .NET Core Runtime 3.1.10 (x64) + .NET Runtime 5.0.1 (x64) \ No newline at end of file diff --git a/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml b/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml index 7744d22..0c12d7b 100644 --- a/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml +++ b/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - 35 + 37 1.0.0.* True Release @@ -19,8 +19,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121. true false Any CPU - bin\publish\ - bin\publish\ + bin\publish-manual\ + bin\publish-manual\ ClickOnce True False @@ -28,14 +28,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121. False sha256RSA True - netcoreapp3.1 + net5.0 False Foreground - + true - .NET Core Runtime 3.1.10 (x64) + .NET Runtime 5.0.1 (x64) \ No newline at end of file diff --git a/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml.user b/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml.user deleted file mode 100644 index a32fee2..0000000 --- a/hass-workstation-service/Properties/PublishProfiles/ClickOnceProfile.pubxml.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/hass-workstation-service/Properties/PublishProfiles/FolderProfile.pubxml b/hass-workstation-service/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..5839fdb --- /dev/null +++ b/hass-workstation-service/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,18 @@ + + + + + Release + Any CPU + bin\Manual\ + FileSystem + net5.0 + true + win-x64 + False + False + False + + \ No newline at end of file diff --git a/hass-workstation-service/Properties/PublishProfiles/SelfContainedClickOnce.pubxml b/hass-workstation-service/Properties/PublishProfiles/SelfContainedClickOnce.pubxml new file mode 100644 index 0000000..811e0e3 --- /dev/null +++ b/hass-workstation-service/Properties/PublishProfiles/SelfContainedClickOnce.pubxml @@ -0,0 +1,35 @@ + + + + + 1 + 1.0.0.* + True + Release + True + True + true + Disk + True + False + 820B7EDF3E26E24BB4C25B177A05B3D0C77BF73A + hass-workstation-service_TemporaryKey.pfx + true + false + Any CPU + bin\clickonce-selfcontained\ + bin\clickonce-selfcontained\ + ClickOnce + False + False + win-x64 + True + sha256RSA + True + net5.0 + False + Foreground + + \ No newline at end of file diff --git a/hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml.user b/hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml.user deleted file mode 100644 index a32fee2..0000000 --- a/hass-workstation-service/Properties/PublishProfiles/SingleFile.pubxml.user +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/hass-workstation-service/UserInterface.exe b/hass-workstation-service/UserInterface.exe index 86049d1..be6aeec 100644 Binary files a/hass-workstation-service/UserInterface.exe and b/hass-workstation-service/UserInterface.exe differ diff --git a/hass-workstation-service/Worker.cs b/hass-workstation-service/Worker.cs index 4b13192..540225b 100644 --- a/hass-workstation-service/Worker.cs +++ b/hass-workstation-service/Worker.cs @@ -9,6 +9,7 @@ using hass_workstation_service.Domain.Sensors; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using MQTTnet.Client; +using Serilog; namespace hass_workstation_service { @@ -51,7 +52,15 @@ namespace hass_workstation_service foreach (AbstractSensor sensor in sensors) { - await sensor.PublishStateAsync(); + try + { + await sensor.PublishStateAsync(); + } + catch (Exception ex) + { + Log.Logger.Warning("Sensor failed: " + sensor.Name, ex); + } + } // announce autodiscovery every 30 seconds if (_mqttPublisher.LastConfigAnnounce < DateTime.UtcNow.AddSeconds(-30)) diff --git a/hass-workstation-service/hass-workstation-service.csproj b/hass-workstation-service/hass-workstation-service.csproj index 0757d67..d75fb12 100644 --- a/hass-workstation-service/hass-workstation-service.csproj +++ b/hass-workstation-service/hass-workstation-service.csproj @@ -1,14 +1,14 @@ - + - netcoreapp3.1 + net5.0 dotnet-hass_workstation_service-C65C2EBE-1977-4C24-AC6B-6921877E1390 hass_workstation_service WinExe Sleevezipper https://github.com/sleevezipper/hass-workstation-service false - 1.0.* + 1.0.0.* false hass-workstation-logo.ico @@ -24,11 +24,15 @@ + + + PreserveNewest + PreserveNewest @@ -45,7 +49,9 @@ - + + + diff --git a/hass-workstation-service/hass-workstation-service.csproj.user b/hass-workstation-service/hass-workstation-service.csproj.user deleted file mode 100644 index 958055e..0000000 --- a/hass-workstation-service/hass-workstation-service.csproj.user +++ /dev/null @@ -1,7 +0,0 @@ - - - - <_LastSelectedProfileId>C:\Users\Maurits\Documents\Repo\hass-desktop-service\hass-workstation-service\Properties\PublishProfiles\AzureHosted.pubxml - true - - \ No newline at end of file diff --git a/hass-workstation-service/hass-workstation-service.exe b/hass-workstation-service/hass-workstation-service.exe new file mode 100644 index 0000000..c309e92 Binary files /dev/null and b/hass-workstation-service/hass-workstation-service.exe differ diff --git a/hass-workstation-service/libSkiaSharp.dll b/hass-workstation-service/libSkiaSharp.dll new file mode 100644 index 0000000..1a27091 Binary files /dev/null and b/hass-workstation-service/libSkiaSharp.dll differ