From d55eaa76f4bd27101a987a32aad33633c8c4fa54 Mon Sep 17 00:00:00 2001 From: sleevezipper Date: Tue, 5 Jan 2021 22:25:19 +0100 Subject: [PATCH] implement webcam and microphone sensors --- README.md | 7 +++ .../ViewModels/AddSensorViewModel.cs | 6 +++ UserInterface/Views/AddSensorDialog.axaml | 2 + UserInterface/Views/AddSensorDialog.axaml.cs | 21 +++++++- .../InterProcessApi.cs | 18 ++++++- .../ServiceContractModels.cs | 9 +++- .../Data/ConfigurationService.cs | 5 +- .../Data/ConfiguredSensor.cs | 2 + .../Domain/Sensors/MicrophoneActiveSensor.cs | 53 +++++++++++++++++++ .../Domain/Sensors/WebcamActiveSensor.cs | 51 ++++++++++++++++-- 10 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs diff --git a/README.md b/README.md index 76ed755..4598747 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,13 @@ 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 has two detection modes: + +- Registry - this is the preferred method. This will work from Windows 10 version 1903 and higher. +- OpenCV - this method tries to access the webcam and if that fails, it assumes it is currently in use. This will flash the webcam activity light at every update interval. It also uses more CPU cycles and memory. + ### 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. diff --git a/UserInterface/ViewModels/AddSensorViewModel.cs b/UserInterface/ViewModels/AddSensorViewModel.cs index 0ea2220..d9cb102 100644 --- a/UserInterface/ViewModels/AddSensorViewModel.cs +++ b/UserInterface/ViewModels/AddSensorViewModel.cs @@ -9,14 +9,18 @@ namespace UserInterface.ViewModels public class AddSensorViewModel : ViewModelBase { private AvailableSensors selectedType; + private WebcamDetectionMode selectedDetectionMode; private string description; private bool showQueryInput; public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); } public bool ShowQueryInput { get => showQueryInput; set => this.RaiseAndSetIfChanged(ref showQueryInput, value); } + public bool ShowDetectionModeOptions { get => showDetectionModeOptions; set => this.RaiseAndSetIfChanged(ref showDetectionModeOptions, value); } + private string moreInfoLink; private int updateInterval; + private bool showDetectionModeOptions; public string MoreInfoLink { @@ -26,6 +30,8 @@ namespace UserInterface.ViewModels public AvailableSensors SelectedType { get => selectedType; set => this.RaiseAndSetIfChanged(ref selectedType, value); } + public WebcamDetectionMode SelectedDetectionMode { get => selectedDetectionMode; set => this.RaiseAndSetIfChanged(ref selectedDetectionMode, value); } + public string Name { get; set; } public string Query { get; set; } public int UpdateInterval { get => updateInterval; set => this.RaiseAndSetIfChanged(ref updateInterval, value); } diff --git a/UserInterface/Views/AddSensorDialog.axaml b/UserInterface/Views/AddSensorDialog.axaml index 7dff3e4..e20d669 100644 --- a/UserInterface/Views/AddSensorDialog.axaml +++ b/UserInterface/Views/AddSensorDialog.axaml @@ -23,6 +23,8 @@ Query + Detection mode + diff --git a/UserInterface/Views/AddSensorDialog.axaml.cs b/UserInterface/Views/AddSensorDialog.axaml.cs index 2f307cd..ae81ab4 100644 --- a/UserInterface/Views/AddSensorDialog.axaml.cs +++ b/UserInterface/Views/AddSensorDialog.axaml.cs @@ -19,6 +19,7 @@ namespace UserInterface.Views { private readonly IIpcClient client; public ComboBox comboBox { get; set; } + public ComboBox detectionModecomboBox { get; set; } public AddSensorDialog() { this.InitializeComponent(); @@ -28,6 +29,9 @@ namespace UserInterface.Views this.comboBox = this.FindControl("ComboBox"); this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast(); + this.comboBox = this.FindControl("DetectionModeComboBox"); + this.comboBox.Items = Enum.GetValues(typeof(WebcamDetectionMode)).Cast(); + // register IPC clients ServiceProvider serviceProvider = new ServiceCollection() .AddNamedPipeIpcClient("addsensor", pipeName: "pipeinternal") @@ -47,7 +51,7 @@ namespace UserInterface.Views public async void Save(object sender, RoutedEventArgs args) { var item = ((AddSensorViewModel)this.DataContext); - dynamic model = new { Name = item.Name, Query = item.Query, UpdateInterval = item.UpdateInterval }; + dynamic model = new { Name = item.Name, Query = item.Query, UpdateInterval = item.UpdateInterval, DetectionMode = item.SelectedDetectionMode }; string json = JsonSerializer.Serialize(model); await this.client.InvokeAsync(x => x.AddSensor(item.SelectedType, json)); Close(); @@ -61,48 +65,63 @@ 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.UpdateInterval = 5; break; case AvailableSensors.DummySensor: item.Description = "This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#dummy"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.UpdateInterval = 1; break; case AvailableSensors.CPULoadSensor: item.Description = "This sensor checks the current CPU load. It averages the load on all logical cores every second and rounds the output to two decimals."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#cpuload"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.UpdateInterval = 5; break; case AvailableSensors.CurrentClockSpeedSensor: item.Description = "This sensor returns the BIOS configured baseclock for the processor."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#currentclockspeed"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.UpdateInterval = 3600; break; case AvailableSensors.WMIQuerySensor: item.Description = "This advanced sensor executes a user defined WMI query and exposes the result. The query should return a single value."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#wmiquerysensor"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = true; 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#usedmemory"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.UpdateInterval = 10; break; case AvailableSensors.ActiveWindowSensor: item.Description = "This sensor exposes the name of the currently active window."; item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#activewindow"; + item.ShowDetectionModeOptions = false; item.ShowQueryInput = false; item.UpdateInterval = 5; break; case AvailableSensors.WebcamActiveSensor: item.Description = "This sensor shows if the webcam is currently in use."; 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; diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs index c45b564..0723a27 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs @@ -107,7 +107,23 @@ namespace hass_workstation_service.Communication.InterProcesCommunication sensorToCreate = new ActiveWindowSensor(this._publisher, (int)model.UpdateInterval, model.Name); break; case AvailableSensors.WebcamActiveSensor: - sensorToCreate = new WebcamActiveSensor(this._publisher, (int)model.UpdateInterval, model.Name); + DetectionMode detectionMode; + switch ((WebcamDetectionMode)model.DetectionMode) + { + case WebcamDetectionMode.Registry: + detectionMode = DetectionMode.Registry; + break; + case WebcamDetectionMode.OpenCV: + detectionMode = DetectionMode.OpenCV; + break; + default: + detectionMode = DetectionMode.Registry; + break; + } + sensorToCreate = new WebcamActiveSensor(this._publisher, (int)model.UpdateInterval, model.Name, detectionMode); + break; + case AvailableSensors.MicrophoneActiveSensor: + sensorToCreate = new MicrophoneActiveSensor(this._publisher, (int)model.UpdateInterval, model.Name); break; default: Log.Logger.Error("Unknown sensortype"); diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs index 3e110ea..f6fd43e 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs @@ -37,6 +37,13 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models WMIQuerySensor, MemoryUsageSensor, ActiveWindowSensor, - WebcamActiveSensor + WebcamActiveSensor, + MicrophoneActiveSensor + } + + public enum WebcamDetectionMode + { + Registry, + OpenCV } } diff --git a/hass-workstation-service/Data/ConfigurationService.cs b/hass-workstation-service/Data/ConfigurationService.cs index 742b27e..e47ca96 100644 --- a/hass-workstation-service/Data/ConfigurationService.cs +++ b/hass-workstation-service/Data/ConfigurationService.cs @@ -89,7 +89,10 @@ namespace hass_workstation_service.Data sensor = new ActiveWindowSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; case "WebcamActiveSensor": - sensor = new WebcamActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); + sensor = new WebcamActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.DetectionMode, configuredSensor.Id); + break; + case "MicrophoneActiveSensor": + sensor = new MicrophoneActiveSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id); break; default: Log.Logger.Error("unsupported sensor type in config"); diff --git a/hass-workstation-service/Data/ConfiguredSensor.cs b/hass-workstation-service/Data/ConfiguredSensor.cs index 06dfd50..f7a4d30 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 @@ -9,5 +10,6 @@ namespace hass_workstation_service.Data public string Name { get; set; } public string Query { get; set; } public int? UpdateInterval { get; set; } + public DetectionMode DetectionMode { get; set; } } } \ No newline at end of file diff --git a/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs new file mode 100644 index 0000000..8c9fa92 --- /dev/null +++ b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs @@ -0,0 +1,53 @@ +using hass_workstation_service.Communication; +using Microsoft.Win32; +using System; +using System.Linq; + +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, updateInterval ?? 10, id) + { + } + public override string GetState() + { + return IsMicrophoneInUse() ? "True" : "False"; + } + public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + { + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + { + Name = this.Name, + Unique_id = this.Id.ToString(), + Device = this.Publisher.DeviceConfigModel, + State_topic = $"homeassistant/sensor/{this.Name}/state", + Icon = "mdi:microphone", + }); + } + + 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/WebcamActiveSensor.cs b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs index aa86cf9..2da9b86 100644 --- a/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs +++ b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs @@ -1,17 +1,35 @@ using hass_workstation_service.Communication; +using Microsoft.Win32; using OpenCvSharp; using System; +using System.Linq; namespace hass_workstation_service.Domain.Sensors { + public enum DetectionMode + { + Registry, + OpenCV + } public class WebcamActiveSensor : AbstractSensor { - public WebcamActiveSensor(MqttPublisher publisher, int? updateInterval = null, string name = "WebcamActive", Guid id = default) : base(publisher, name, updateInterval ?? 10, id) + public DetectionMode DetectionMode { get; private set; } + public WebcamActiveSensor(MqttPublisher publisher, int? updateInterval = null, string name = "WebcamActive", DetectionMode detectionMode = DetectionMode.Registry, Guid id = default(Guid)) : base(publisher, name, updateInterval ?? 10, id) { + this.DetectionMode = detectionMode; } public override string GetState() { - return IsWebCamInUse() ? "True" : "False"; + switch (this.DetectionMode) + { + case DetectionMode.Registry: + return IsWebCamInUseRegistry() ? "True" : "False"; + case DetectionMode.OpenCV: + return IsWebCamInUseOpenCV() ? "True" : "False"; + default: + return "Error"; + } + } public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() { @@ -25,7 +43,7 @@ namespace hass_workstation_service.Domain.Sensors }); } - private bool IsWebCamInUse() + private bool IsWebCamInUseOpenCV() { try { @@ -35,13 +53,13 @@ namespace hass_workstation_service.Domain.Sensors { capture.Release(); capture.Dispose(); - return true; + return false; } else { capture.Release(); capture.Dispose(); - return false; + return true; } } @@ -51,5 +69,28 @@ namespace hass_workstation_service.Domain.Sensors return false; } } + + 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; + } } }