Merge branch 'develop'

pull/19/head v1.0.0.638
sleevezipper 4 years ago
commit 776fd6274b

2
.gitignore vendored

@ -1,2 +1,4 @@
.vs/
*.user

@ -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.

@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform>
<PublishDir>..\hass-workstation-service\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>false</SelfContained>
<PublishSingleFile>True</PublishSingleFile>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Userinterface-standalone</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net5.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishTrimmed>False</PublishTrimmed>
</PropertyGroup>
</Project>

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
@ -17,6 +17,7 @@
<PackageReference Include="Avalonia.ReactiveUI" Version="0.9.12" />
<PackageReference Include="Avalonia.Win32" Version="0.9.12" />
<PackageReference Include="JKang.IpcServiceFramework.Client.NamedPipe" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\hass-workstation-service\hass-workstation-service.csproj" />

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\Maurits\Documents\Repo\hass-desktop-service\UserInterface\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

@ -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; }

@ -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)

@ -40,7 +40,7 @@ namespace UserInterface.ViewModels
{
if (!string.IsNullOrWhiteSpace(_value))
{
return _value + UnitOfMeasurement;
return _value + " " + UnitOfMeasurement;
}
else return "";

@ -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>(T obj, out ICollection<ValidationResult> results)
{
results = new List<ValidationResult>();
return Validator.TryValidateObject(obj, new ValidationContext(obj), results, true);
}
}
}

@ -26,6 +26,6 @@
<ContentControl IsVisible="{Binding ShowWindowNameInput}" Margin="0 20 0 5">Window name</ContentControl>
<TextBlock TextWrapping="Wrap" MaxWidth="300" FontStyle="Italic" IsVisible="{Binding ShowWindowNameInput}" Margin="0 0 0 10">This is case-insensitive and loosely matched. A window called "Spotify Premium" will match "spotify" or "premium".</TextBlock>
<TextBox IsVisible="{Binding ShowWindowNameInput}" Text="{Binding WindowName}" Watermark="Visual Studio Code" HorizontalAlignment="Left" MinWidth="300"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Save">Save</Button>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Save">Save</Button>
</StackPanel>
</Window>

@ -19,14 +19,17 @@ namespace UserInterface.Views
{
private readonly IIpcClient<ServiceContractInterfaces> 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>("ComboBox");
this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast<AvailableSensors>();
this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast<AvailableSensors>().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;

@ -10,6 +10,17 @@
<TextBlock IsVisible="{Binding !IsConnected}" Foreground="Red" Text="{Binding Message}"></TextBlock >
<ContentControl Margin="0 20 0 10">IP or hostname</ContentControl>
<TextBox Text="{Binding Host}" HorizontalAlignment="Left" Width="100" Watermark="192.168.1.200"/>
<StackPanel Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<ContentControl Margin="0 20 0 10">Port</ContentControl>
<TextBox Text="{Binding Port}" HorizontalAlignment="Left" Width="50" Watermark="1883"/>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="30 0 0 0">
<ContentControl Margin="0 20 0 10">Use TLS</ContentControl>
<CheckBox IsChecked="{Binding UseTLS}" HorizontalAlignment="Left" Margin="0 3 0 0"/>
</StackPanel>
</StackPanel>
<ContentControl Margin="0 20 0 10">Username</ContentControl>
<TextBox Text="{Binding Username}" MinWidth="150"/>
<ContentControl Margin="0 20 0 10">Password</ContentControl>

@ -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<ValidationResult> 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()

@ -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;

@ -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
}
}

@ -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()

@ -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<AbstractSensor> ConfiguredSensors { get; private set; }
public Action<IMqttClientOptions> 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<AbstractSensor>();
@ -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<ConfiguredSensor> sensors = new List<ConfiguredSensor>();
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<List<ConfiguredSensor>>(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
/// <returns></returns>
public async Task<ConfiguredMqttBroker> 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<ConfiguredSensor> configuredSensorsToSave = new List<ConfiguredSensor>();
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
/// <param name="settings"></param>
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
/// </summary>
/// <param name="enable"></param>
[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);

@ -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;
}
}
}

@ -1,3 +1,4 @@
using hass_workstation_service.Domain.Sensors;
using System;
namespace hass_workstation_service.Data

@ -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();
}

@ -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",
});
}

@ -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();

@ -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"
});

@ -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"
});
}

@ -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;
}
}
}

@ -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 = "%"
});

@ -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;
}
}
}

@ -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",
});
}

@ -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
{
/// <summary>
/// all users are locked
/// </summary>
Locked,
/// <summary>
/// No users are logged in
/// </summary>
LoggedOff,
/// <summary>
/// A user is using this computer
/// </summary>
InUse,
/// <summary>
/// unable to connect to computer / other error
/// </summary>
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<ManagementObject>()
.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;
}
}
}
}

@ -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();
}
}

@ -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",
});
}

@ -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",
});
}

@ -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;
}
}
}

@ -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()

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApplicationRevision>25</ApplicationRevision>
<ApplicationRevision>27</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
@ -33,15 +33,15 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SignatureAlgorithm>sha256RSA</SignatureAlgorithm>
<SignManifests>True</SignManifests>
<SupportUrl>https://github.com/sleevezipper/hass-workstation-service</SupportUrl>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<TrustUrlParameters>True</TrustUrlParameters>
<UpdateEnabled>True</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
</PropertyGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.3.1.x64">
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.5.0.x64">
<Install>true</Install>
<ProductName>.NET Core Runtime 3.1.10 (x64)</ProductName>
<ProductName>.NET Runtime 5.0.1 (x64)</ProductName>
</BootstrapperPackage>
</ItemGroup>
</Project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApplicationRevision>1</ApplicationRevision>
<ApplicationRevision>10</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
@ -19,23 +19,25 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<MapFileExtensions>true</MapFileExtensions>
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<PublishDir>bin\publish\</PublishDir>
<PublishUrl>bin\publish\</PublishUrl>
<PublishDir>bin\clickonce-framework-dependent\</PublishDir>
<PublishUrl>bin\clickonce-framework-dependent\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>True</PublishReadyToRun>
<PublishSingleFile>True</PublishSingleFile>
<PublisherName>Sleevezipper</PublisherName>
<ProductName>Hass Workstation Service</ProductName>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishSingleFile>False</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>False</SelfContained>
<SignatureAlgorithm>sha256RSA</SignatureAlgorithm>
<SignManifests>True</SignManifests>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<UpdateEnabled>False</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
</PropertyGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.3.1.x64">
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.5.0.x64">
<Install>true</Install>
<ProductName>.NET Core Runtime 3.1.10 (x64)</ProductName>
<ProductName>.NET Runtime 5.0.1 (x64)</ProductName>
</BootstrapperPackage>
</ItemGroup>
</Project>

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApplicationRevision>35</ApplicationRevision>
<ApplicationRevision>37</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
@ -19,8 +19,8 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<MapFileExtensions>true</MapFileExtensions>
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<PublishDir>bin\publish\</PublishDir>
<PublishUrl>bin\publish\</PublishUrl>
<PublishDir>bin\publish-manual\</PublishDir>
<PublishUrl>bin\publish-manual\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>True</PublishReadyToRun>
<PublishSingleFile>False</PublishSingleFile>
@ -28,14 +28,14 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<SelfContained>False</SelfContained>
<SignatureAlgorithm>sha256RSA</SignatureAlgorithm>
<SignManifests>True</SignManifests>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<UpdateEnabled>False</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
</PropertyGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.3.1.x64">
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.5.0.x64">
<Install>true</Install>
<ProductName>.NET Core Runtime 3.1.10 (x64)</ProductName>
<ProductName>.NET Runtime 5.0.1 (x64)</ProductName>
</BootstrapperPackage>
</ItemGroup>
</Project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Manual\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net5.0</TargetFramework>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishSingleFile>False</PublishSingleFile>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishTrimmed>False</PublishTrimmed>
</PropertyGroup>
</Project>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApplicationRevision>1</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
<CreateDesktopShortcut>True</CreateDesktopShortcut>
<GenerateManifests>True</GenerateManifests>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<IsRevisionIncremented>True</IsRevisionIncremented>
<IsWebBootstrapper>False</IsWebBootstrapper>
<ManifestCertificateThumbprint>820B7EDF3E26E24BB4C25B177A05B3D0C77BF73A</ManifestCertificateThumbprint>
<ManifestKeyFile>hass-workstation-service_TemporaryKey.pfx</ManifestKeyFile>
<MapFileExtensions>true</MapFileExtensions>
<OpenBrowserOnPublish>false</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<PublishDir>bin\clickonce-selfcontained\</PublishDir>
<PublishUrl>bin\clickonce-selfcontained\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishSingleFile>False</PublishSingleFile>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>True</SelfContained>
<SignatureAlgorithm>sha256RSA</SignatureAlgorithm>
<SignManifests>True</SignManifests>
<TargetFramework>net5.0</TargetFramework>
<UpdateEnabled>False</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
</PropertyGroup>
</Project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
</Project>

@ -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))

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net5.0</TargetFramework>
<UserSecretsId>dotnet-hass_workstation_service-C65C2EBE-1977-4C24-AC6B-6921877E1390</UserSecretsId>
<RootNamespace>hass_workstation_service</RootNamespace>
<OutputType>WinExe</OutputType>
<Authors>Sleevezipper</Authors>
<RepositoryUrl>https://github.com/sleevezipper/hass-workstation-service</RepositoryUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<AssemblyVersion>1.0.*</AssemblyVersion>
<AssemblyVersion>1.0.0.*</AssemblyVersion>
<Deterministic>false</Deterministic>
<ApplicationIcon>hass-workstation-logo.ico</ApplicationIcon>
</PropertyGroup>
@ -24,11 +24,15 @@
</ItemGroup>
<ItemGroup>
<None Remove="libSkiaSharp.dll" />
<None Remove="UserInterface.exe" />
<None Remove="UserInterface.pdb" />
</ItemGroup>
<ItemGroup>
<Content Include="libSkiaSharp.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="UserInterface.exe">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
@ -45,7 +49,9 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MQTTnet" Version="3.0.13" />
<PackageReference Include="Serilog.AspNetCore" Version="3.4.0" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="System.Management" Version="5.0.0" />
</ItemGroup>
</Project>

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>C:\Users\Maurits\Documents\Repo\hass-desktop-service\hass-workstation-service\Properties\PublishProfiles\AzureHosted.pubxml</_LastSelectedProfileId>
<ShowAllFiles>true</ShowAllFiles>
</PropertyGroup>
</Project>
Loading…
Cancel
Save