Merge branch 'develop'

pull/34/head v1.0.0.23274
sleevezipper 4 years ago
commit 7684d65fd0

@ -15,9 +15,9 @@ It will try to futher accomplish this goal in the future by:
## Screenshots ## Screenshots
![The settings screen](https://i.imgur.com/WpCZaDR.png) ![The settings screen](https://i.imgur.com/RBQx807.png)
![The resulting sensors in Home Assistant](https://i.imgur.com/Kka8VOi.png) ![The resulting sensors and commands in Home Assistant](https://i.imgur.com/jXRU2cu.png)
## Installation ## Installation
@ -34,6 +34,10 @@ If you don't want to use the installer, you can find the standalone version rele
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. 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.
## Need help?
Find us on us on [Discord](https://discord.gg/VraYT2N3wd).
## Sensors ## 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/{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. 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.
@ -132,3 +136,29 @@ This sensor returns the current session state. It has the following possible sta
### Dummy ### Dummy
This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it. This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.
## Commands
Commands can be used to trigger certain things on the client. For each command, a switch will be available in Home Assistant. Turning on the switch fires the command on the client and it will turn the switch off when it's done. Turning it off will cancel thje running command.
### ShutdownCommand
This command shuts down the computer immediately. It runs `shutdown /s`.
### RestartCommand
This command restarts the computer immediately. It runs `shutdown /r`.
### LogOffCommand
This command logs off the current user. It runs `shutdown /l`.
### CustomCommand
This command allows you to run any Windows Commands. The command will be run in a hidden Command Prompt. Some examples:
|Command|Explanation|
|---|---|
|Rundll32.exe user32.dll,LockWorkStation|This locks the current session.|
|shutdown /s /t 300|Shuts the PC down after 5 minutes (300 seconds).|
|C:\path\to\your\batchfile.bat|Run the specified batch file.|

@ -23,9 +23,18 @@
<ProjectReference Include="..\hass-workstation-service\hass-workstation-service.csproj" /> <ProjectReference Include="..\hass-workstation-service\hass-workstation-service.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Update="Views\AddCommandDialog.axaml.cs">
<DependentUpon>AddCommandDialog.axaml</DependentUpon>
</Compile>
<Compile Update="Views\AppInfo.axaml.cs">
<DependentUpon>AppInfo.axaml</DependentUpon>
</Compile>
<Compile Update="Views\BackgroundServiceSettings.axaml.cs"> <Compile Update="Views\BackgroundServiceSettings.axaml.cs">
<DependentUpon>BackgroundServiceSettings.axaml</DependentUpon> <DependentUpon>BackgroundServiceSettings.axaml</DependentUpon>
</Compile> </Compile>
<Compile Update="Views\CommandSettings.axaml.cs">
<DependentUpon>CommandSettings.axaml</DependentUpon>
</Compile>
<Compile Update="Views\SensorSettings.axaml.cs"> <Compile Update="Views\SensorSettings.axaml.cs">
<DependentUpon>SensorSettings.axaml</DependentUpon> <DependentUpon>SensorSettings.axaml</DependentUpon>
</Compile> </Compile>

@ -0,0 +1,31 @@
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class AddCommandViewModel : ViewModelBase
{
private AvailableCommands selectedType;
private string description;
public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); }
public bool ShowCommandInput { get => showCommandInput; set => this.RaiseAndSetIfChanged(ref showCommandInput, value); }
private string moreInfoLink;
private bool showCommandInput;
public string MoreInfoLink
{
get { return moreInfoLink; }
set { this.RaiseAndSetIfChanged(ref moreInfoLink, value); }
}
public AvailableCommands SelectedType { get => selectedType; set => this.RaiseAndSetIfChanged(ref selectedType, value); }
public string Name { get; set; }
public string Command { get; set; }
}
}

@ -0,0 +1,25 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class CommandSettingsViewModel : ViewModelBase
{
private ICollection<CommandViewModel> configuredCommands;
public ICollection<CommandViewModel> ConfiguredCommands { get => configuredCommands; set => this.RaiseAndSetIfChanged(ref configuredCommands, value); }
public void TriggerUpdate()
{
this.RaisePropertyChanged();
}
}
public class CommandViewModel : ViewModelBase
{
public Guid Id { get; set; }
public string Type { get; set; }
public string Name { get; set; }
}
}

@ -0,0 +1,26 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="UserInterface.Views.AddCommandDialog"
SizeToContent="WidthAndHeight"
Title="Add sensor">
<StackPanel Margin="40" MinWidth="200">
<ContentControl Margin="0 20 0 10">Sensor type</ContentControl>
<ComboBox x:Name="ComboBox" SelectionChanged="ComboBoxClosed" SelectedItem="{Binding SelectedType}" MinHeight="27"></ComboBox>
<TextBlock Margin="0 10 0 10" MaxWidth="300" TextWrapping="Wrap" TextAlignment="Left" Text="{Binding Description}"></TextBlock>
<Button IsVisible="{Binding MoreInfoLink, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Click="OpenInfo" Margin="0 10 0 10">Click for more information.</Button>
<ContentControl Margin="0 20 0 10">Name</ContentControl>
<TextBox Text="{Binding Name}" HorizontalAlignment="Left" MinWidth="150"/>
<TextBlock Text="{Binding UpdateInterval, StringFormat= Update every {0} seconds}" HorizontalAlignment="Left" MinWidth="150"/>
<ContentControl IsVisible="{Binding ShowCommandInput}" Margin="0 20 0 10">Command</ContentControl>
<TextBox IsVisible="{Binding ShowCommandInput}" Text="{Binding Command}" Watermark="Rundll32.exe user32.dll,LockWorkStation" HorizontalAlignment="Left" MinWidth="300"/>
<Button IsVisible="{Binding ShowCommandInput}" Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Test">Test</Button>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Save">Save</Button>
</StackPanel>
</Window>

@ -0,0 +1,112 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.NamedPipe;
using JKang.IpcServiceFramework.Client;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Dynamic;
using System.Linq;
using System.Text.Json;
using UserInterface.Util;
using UserInterface.ViewModels;
namespace UserInterface.Views
{
public class AddCommandDialog : Window
{
private readonly IIpcClient<ServiceContractInterfaces> client;
public ComboBox comboBox { get; set; }
public ComboBox detectionModecomboBox { get; set; }
public AddCommandDialog()
{
this.InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
DataContext = new AddCommandViewModel();
this.comboBox = this.FindControl<ComboBox>("ComboBox");
this.comboBox.Items = Enum.GetValues(typeof(AvailableCommands)).Cast<AvailableCommands>().OrderBy(v => v.ToString());
this.comboBox.SelectedIndex = 0;
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("addCommand", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("addCommand");
}
public async void Save(object sender, RoutedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
dynamic model = new { item.Name, item.Command};
string json = JsonSerializer.Serialize(model);
await this.client.InvokeAsync(x => x.AddCommand(item.SelectedType, json));
Close();
}
public void ComboBoxClosed(object sender, SelectionChangedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
switch (item.SelectedType)
{
case AvailableCommands.CustomCommand:
item.Description = "This command lets you execute any command you want. It will run in a Windows Command Prompt silently. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#customcommand";
item.ShowCommandInput = true;
break;
case AvailableCommands.ShutdownCommand:
item.Description = "This command shuts down the PC immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#shutdowncommand";
item.ShowCommandInput = false;
break;
case AvailableCommands.RestartCommand:
item.Description = "This command restarts the PC immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#restartcommand";
item.ShowCommandInput = false;
break;
case AvailableCommands.LogOffCommand:
item.Description = "This command logs the current user off immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#logoffcommand";
item.ShowCommandInput = false;
break;
default:
item.Description = null;
item.MoreInfoLink = null;
item.ShowCommandInput = false;
break;
}
}
public void OpenInfo(object sender, RoutedEventArgs args)
{
var item = ((AddSensorViewModel)this.DataContext);
BrowserUtil.OpenBrowser(item.MoreInfoLink);
}
public void Test(object sender, RoutedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
System.Diagnostics.Process process = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = $"/k {"echo You won't see this window normally. &&" + item.Command}";
process.StartInfo = startInfo;
process.Start();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -0,0 +1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="UserInterface.Views.AppInfo">
<StackPanel Margin="30">
<StackPanel Margin="0 0 0 20" HorizontalAlignment="Left">
<ContentControl FontSize="18" FontWeight="Bold" >Info</ContentControl>
<TextBlock Text="Need some help?" Margin="0 0 0 20"></TextBlock >
<StackPanel Margin="0 0 0 20" HorizontalAlignment="Left" Orientation="Horizontal">
<Button Width="75" HorizontalAlignment="Right" Margin="10 10" Click="Github">Github</Button>
<Button Width="75" HorizontalAlignment="Left" Margin="10 10" Click="Discord">Discord</Button>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

@ -0,0 +1,91 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using hass_workstation_service.Communication.NamedPipe;
using JKang.IpcServiceFramework.Client;
using System.Threading.Tasks;
using Avalonia.Interactivity;
using System.Reactive.Linq;
using UserInterface.ViewModels;
using System.Security;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using UserInterface.Util;
namespace UserInterface.Views
{
public class AppInfo : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
public AppInfo()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("broker", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("broker");
DataContext = new BackgroundServiceSettingsViewModel();
Ping();
}
public async void Ping() {
while (true)
{
try
{
var result = await this.client.InvokeAsync(x => x.Ping("ping"));
if (result == "pong")
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(true, "All good");
}
else
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(false, "Not running");
}
}
catch (System.Exception)
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(false, "Not running");
}
var autostartresult = await this.client.InvokeAsync(x => x.IsAutoStartEnabled());
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateAutostartStatus(autostartresult);
await Task.Delay(1000);
}
}
public void Github(object sender, RoutedEventArgs args)
{
BrowserUtil.OpenBrowser("https://github.com/sleevezipper/hass-workstation-service");
}
public void Discord(object sender, RoutedEventArgs args)
{
BrowserUtil.OpenBrowser("https://discord.gg/VraYT2N3wd");
}
public void EnableAutostart(object sender, RoutedEventArgs args)
{
this.client.InvokeAsync(x => x.EnableAutostart(true));
}
public void DisableAutostart(object sender, RoutedEventArgs args)
{
this.client.InvokeAsync(x => x.EnableAutostart(false));
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MaxWidth="800"
x:Class="UserInterface.Views.CommandSettings" >
<StackPanel Margin="30" HorizontalAlignment="Left" >
<ContentControl FontSize="18" Margin="0 0 0 15" FontWeight="Bold">Commands</ContentControl>
<DataGrid x:Name="Grid" IsVisible="{Binding ConfiguredCommands.Count}" AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single" Items="{Binding ConfiguredCommands}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"
Width="1*" />
<DataGridTextColumn Header="Type"
Binding="{Binding Type}"
Width="1*" />
</DataGrid.Columns>
</DataGrid>
<TextBlock IsVisible="{Binding !ConfiguredCommands.Count}">Add some commands by clicking the "Add" button. </TextBlock>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Add">Add</Button>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Delete">Delete</Button>
</StackPanel>
</UserControl>

@ -0,0 +1,82 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using hass_workstation_service.Communication.NamedPipe;
using JKang.IpcServiceFramework.Client;
using System.Threading.Tasks;
using Avalonia.Interactivity;
using System.Reactive.Linq;
using UserInterface.ViewModels;
using System.Security;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.ApplicationLifetimes;
namespace UserInterface.Views
{
public class CommandSettings : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private DataGrid _dataGrid { get; set; }
private bool sensorsNeedToRefresh { get; set; }
public CommandSettings()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("commands", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("commands");
DataContext = new CommandSettingsViewModel();
GetConfiguredCommands();
this._dataGrid = this.FindControl<DataGrid>("Grid");
}
public async void GetConfiguredCommands()
{
sensorsNeedToRefresh = false;
List<ConfiguredCommandModel> status = await this.client.InvokeAsync(x => x.GetConfiguredCommands());
((CommandSettingsViewModel)this.DataContext).ConfiguredCommands = status.Select(s => new CommandViewModel() { Name = s.Name, Type = s.Type, Id = s.Id}).ToList();
}
public void Delete(object sender, RoutedEventArgs args)
{
var item = ((CommandViewModel)this._dataGrid.SelectedItem);
this.client.InvokeAsync(x => x.RemoveCommandById(item.Id));
((CommandSettingsViewModel)this.DataContext).ConfiguredCommands.Remove(item);
this._dataGrid.SelectedIndex = -1;
((CommandSettingsViewModel)this.DataContext).TriggerUpdate();
}
public async void Add(object sender, RoutedEventArgs args)
{
AddCommandDialog dialog = new AddCommandDialog();
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
await dialog.ShowDialog(desktop.MainWindow);
sensorsNeedToRefresh = true;
GetConfiguredCommands();
}
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -14,9 +14,11 @@
</Design.DataContext> </Design.DataContext>
<Grid ColumnDefinitions="Auto,1*,Auto" RowDefinitions="Auto,Auto,Auto" Margin="30"> <Grid ColumnDefinitions="Auto,1*,Auto" RowDefinitions="Auto,Auto,Auto" Margin="30">
<views:BrokerSettings Grid.Column="0" Grid.Row="0" Margin="10 0" Background="#2D2D30"/> <views:BrokerSettings Grid.Column="0" Grid.Row="0" Margin="10 0" Grid.RowSpan="2" Background="#2D2D30"/>
<views:SensorSettings Grid.Column="1" Grid.Row="0" Margin="10 0" Background="#2D2D30"/> <views:SensorSettings Grid.Column="1" Grid.Row="0" Margin="10 0" Background="#2D2D30"/>
<views:CommandSettings Grid.Column="1" Grid.Row="1" Margin="0 10 0 0" Background="#2D2D30"/>
<views:BackgroundServiceSettings Grid.Column="2" Grid.Row="0" Margin="10 0" Background="#2D2D30"/> <views:BackgroundServiceSettings Grid.Column="2" Grid.Row="0" Margin="10 0" Background="#2D2D30"/>
<views:AppInfo Grid.Column="2" Grid.Row="1" Margin="10 10 10 0" Background="#2D2D30"/>
<!--<views:BrokerSettings Grid.Column="1" Grid.Row="0"/> <!--<views:BrokerSettings Grid.Column="1" Grid.Row="0"/>
<views:BrokerSettings Grid.Column="2" Grid.Row="0"/>--> <views:BrokerSettings Grid.Column="2" Grid.Row="0"/>-->

@ -6,6 +6,7 @@
MaxWidth="800" MaxWidth="800"
x:Class="UserInterface.Views.SensorSettings" > x:Class="UserInterface.Views.SensorSettings" >
<StackPanel Margin="30" HorizontalAlignment="Left" > <StackPanel Margin="30" HorizontalAlignment="Left" >
<ContentControl FontSize="18" Margin="0 0 0 15" FontWeight="Bold">Sensors</ContentControl>
<DataGrid x:Name="Grid" IsVisible="{Binding ConfiguredSensors.Count}" AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single" Items="{Binding ConfiguredSensors}"> <DataGrid x:Name="Grid" IsVisible="{Binding ConfiguredSensors.Count}" AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single" Items="{Binding ConfiguredSensors}">
<DataGrid.Columns> <DataGrid.Columns>

@ -2,6 +2,7 @@ using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.NamedPipe; using hass_workstation_service.Communication.NamedPipe;
using hass_workstation_service.Communication.Util; using hass_workstation_service.Communication.Util;
using hass_workstation_service.Data; using hass_workstation_service.Data;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Serilog; using Serilog;
using System; using System;
@ -49,11 +50,19 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
return "what?"; return "what?";
} }
/// <summary>
/// This writes the provided settings to the config file.
/// </summary>
/// <param name="settings"></param>
public void WriteMqttBrokerSettingsAsync(MqttSettings settings) public void WriteMqttBrokerSettingsAsync(MqttSettings settings)
{ {
this._configurationService.WriteMqttBrokerSettingsAsync(settings); this._configurationService.WriteMqttBrokerSettingsAsync(settings);
} }
/// <summary>
/// Enables or disables autostart.
/// </summary>
/// <param name="enable"></param>
public void EnableAutostart(bool enable) public void EnableAutostart(bool enable)
{ {
this._configurationService.EnableAutoStart(enable); this._configurationService.EnableAutoStart(enable);
@ -69,11 +78,25 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
return this._configurationService.ConfiguredSensors.Select(s => new ConfiguredSensorModel() { Name = s.Name, Type = s.GetType().Name, Value = s.PreviousPublishedState, Id = s.Id, UpdateInterval = s.UpdateInterval, UnitOfMeasurement = s.GetAutoDiscoveryConfig().Unit_of_measurement }).ToList(); return this._configurationService.ConfiguredSensors.Select(s => new ConfiguredSensorModel() { Name = s.Name, Type = s.GetType().Name, Value = s.PreviousPublishedState, Id = s.Id, UpdateInterval = s.UpdateInterval, UnitOfMeasurement = s.GetAutoDiscoveryConfig().Unit_of_measurement }).ToList();
} }
public List<ConfiguredCommandModel> GetConfiguredCommands()
{
return this._configurationService.ConfiguredCommands.Select(s => new ConfiguredCommandModel() { Name = s.Name, Type = s.GetType().Name, Id = s.Id }).ToList();
}
public void RemoveCommandById(Guid id)
{
this._configurationService.DeleteConfiguredCommand(id);
}
public void RemoveSensorById(Guid id) public void RemoveSensorById(Guid id)
{ {
this._configurationService.DeleteConfiguredSensor(id); this._configurationService.DeleteConfiguredSensor(id);
} }
/// <summary>
/// Adds a command to the configured commands. This properly initializes the class and writes it to the config file.
/// </summary>
/// <param name="sensorType"></param>
/// <param name="json"></param>
public void AddSensor(AvailableSensors sensorType, string json) public void AddSensor(AvailableSensors sensorType, string json)
{ {
var serializerOptions = new JsonSerializerOptions var serializerOptions = new JsonSerializerOptions
@ -133,5 +156,43 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
this._configurationService.AddConfiguredSensor(sensorToCreate); this._configurationService.AddConfiguredSensor(sensorToCreate);
} }
} }
/// <summary>
/// Adds a command to the configured commands. This properly initializes the class, subscribes to the command topic and writes it to the config file.
/// </summary>
/// <param name="commandType"></param>
/// <param name="json"></param>
public void AddCommand(AvailableCommands commandType, string json)
{
var serializerOptions = new JsonSerializerOptions
{
Converters = { new DynamicJsonConverter() }
};
dynamic model = JsonSerializer.Deserialize<dynamic>(json, serializerOptions);
AbstractCommand commandToCreate = null;
switch (commandType)
{
case AvailableCommands.ShutdownCommand:
commandToCreate = new ShutdownCommand(this._publisher, model.Name);
break;
case AvailableCommands.RestartCommand:
commandToCreate = new RestartCommand(this._publisher, model.Name);
break;
case AvailableCommands.LogOffCommand:
commandToCreate = new LogOffCommand(this._publisher, model.Name);
break;
case AvailableCommands.CustomCommand:
commandToCreate = new CustomCommand(this._publisher, model.Command, model.Name);
break;
default:
Log.Logger.Error("Unknown sensortype");
break;
}
if (commandToCreate != null)
{
this._configurationService.AddConfiguredCommand(commandToCreate);
}
}
} }
} }

@ -17,5 +17,8 @@ namespace hass_workstation_service.Communication.NamedPipe
List<ConfiguredSensorModel> GetConfiguredSensors(); List<ConfiguredSensorModel> GetConfiguredSensors();
void RemoveSensorById(Guid id); void RemoveSensorById(Guid id);
void AddSensor(AvailableSensors sensorType, string json); void AddSensor(AvailableSensors sensorType, string json);
void RemoveCommandById(Guid id);
List<ConfiguredCommandModel> GetConfiguredCommands();
void AddCommand(AvailableCommands commandType, string json);
} }
} }

@ -29,7 +29,13 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
public int UpdateInterval { get; set; } public int UpdateInterval { get; set; }
public string UnitOfMeasurement { get; set; } public string UnitOfMeasurement { get; set; }
} }
public class ConfiguredCommandModel
{
public Guid Id { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public string Command { get; set; }
}
public enum AvailableSensors public enum AvailableSensors
{ {
UserNotificationStateSensor, UserNotificationStateSensor,
@ -46,4 +52,12 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
LastBootSensor, LastBootSensor,
SessionStateSensor SessionStateSensor
} }
public enum AvailableCommands
{
CustomCommand,
ShutdownCommand,
LogOffCommand,
RestartCommand,
}
} }

@ -1,16 +1,20 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.Util; using hass_workstation_service.Communication.Util;
using hass_workstation_service.Data; using hass_workstation_service.Data;
using hass_workstation_service.Domain.Commands;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MQTTnet; using MQTTnet;
using MQTTnet.Adapter; using MQTTnet.Adapter;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using MQTTnet.Exceptions; using MQTTnet.Exceptions;
using MQTTnet.Extensions.ManagedClient;
using Serilog; using Serilog;
namespace hass_workstation_service.Communication namespace hass_workstation_service.Communication
@ -18,12 +22,14 @@ namespace hass_workstation_service.Communication
public class MqttPublisher public class MqttPublisher
{ {
private readonly IMqttClient _mqttClient; private readonly IManagedMqttClient _mqttClient;
private readonly ILogger<MqttPublisher> _logger; private readonly ILogger<MqttPublisher> _logger;
private readonly IConfigurationService _configurationService; private readonly IConfigurationService _configurationService;
private string _mqttClientMessage { get; set; } private string _mqttClientMessage { get; set; }
public DateTime LastConfigAnnounce { get; private set; } public DateTime LastConfigAnnounce { get; private set; }
public DateTime LastAvailabilityAnnounce { get; private set; }
public DeviceConfigModel DeviceConfigModel { get; private set; } public DeviceConfigModel DeviceConfigModel { get; private set; }
public ICollection<AbstractCommand> Subscribers { get; private set; }
public bool IsConnected public bool IsConnected
{ {
get get
@ -44,7 +50,7 @@ namespace hass_workstation_service.Communication
DeviceConfigModel deviceConfigModel, DeviceConfigModel deviceConfigModel,
IConfigurationService configurationService) IConfigurationService configurationService)
{ {
this.Subscribers = new List<AbstractCommand>();
this._logger = logger; this._logger = logger;
this.DeviceConfigModel = deviceConfigModel; this.DeviceConfigModel = deviceConfigModel;
this._configurationService = configurationService; this._configurationService = configurationService;
@ -53,11 +59,11 @@ namespace hass_workstation_service.Communication
_configurationService.MqqtConfigChangedHandler = this.ReplaceMqttClient; _configurationService.MqqtConfigChangedHandler = this.ReplaceMqttClient;
var factory = new MqttFactory(); var factory = new MqttFactory();
this._mqttClient = factory.CreateMqttClient(); this._mqttClient = factory.CreateManagedMqttClient();
if (options != null) if (options != null)
{ {
this._mqttClient.ConnectAsync(options); this._mqttClient.StartAsync(options);
this._mqttClientMessage = "Connecting..."; this._mqttClientMessage = "Connecting...";
} }
else else
@ -68,25 +74,12 @@ namespace hass_workstation_service.Communication
this._mqttClient.UseConnectedHandler(e => { this._mqttClient.UseConnectedHandler(e => {
this._mqttClientMessage = "All good"; this._mqttClientMessage = "All good";
}); });
this._mqttClient.UseApplicationMessageReceivedHandler(e => this.HandleMessageReceived(e.ApplicationMessage));
// configure what happens on disconnect // configure what happens on disconnect
this._mqttClient.UseDisconnectedHandler(async e => this._mqttClient.UseDisconnectedHandler(e =>
{ {
this._mqttClientMessage = e.ReasonCode.ToString(); this._mqttClientMessage = e.ReasonCode.ToString();
if (e.ReasonCode != MQTTnet.Client.Disconnecting.MqttClientDisconnectReason.NormalDisconnection)
{
_logger.LogWarning("Disconnected from server");
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
await this._mqttClient.ConnectAsync(options, CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Reconnecting failed");
}
}
}); });
} }
@ -103,7 +96,7 @@ namespace hass_workstation_service.Communication
} }
} }
public async Task AnnounceAutoDiscoveryConfig(AutoDiscoveryConfigModel config, bool clearConfig = false) public async Task AnnounceAutoDiscoveryConfig(DiscoveryConfigModel config, string domain, bool clearConfig = false)
{ {
if (this._mqttClient.IsConnected) if (this._mqttClient.IsConnected)
{ {
@ -111,11 +104,13 @@ namespace hass_workstation_service.Communication
{ {
PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(), PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(),
IgnoreNullValues = true, IgnoreNullValues = true,
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true,
}; };
var message = new MqttApplicationMessageBuilder() var message = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/sensor/{this.DeviceConfigModel.Name}/{config.Name}/config") .WithTopic($"homeassistant/{domain}/{this.DeviceConfigModel.Name}/{config.Name}/config")
.WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, options)) .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, config.GetType(), options))
.WithRetainFlag() .WithRetainFlag()
.Build(); .Build();
await this.Publish(message); await this.Publish(message);
@ -123,13 +118,13 @@ namespace hass_workstation_service.Communication
} }
} }
public async void ReplaceMqttClient(IMqttClientOptions options) public async void ReplaceMqttClient(IManagedMqttClientOptions options)
{ {
this._logger.LogInformation($"Replacing Mqtt client with new config"); this._logger.LogInformation($"Replacing Mqtt client with new config");
await _mqttClient.DisconnectAsync(); await _mqttClient.StopAsync();
try try
{ {
await _mqttClient.ConnectAsync(options); await _mqttClient.StartAsync(options);
} }
catch (MqttConnectingFailedException ex) catch (MqttConnectingFailedException ex)
{ {
@ -147,5 +142,74 @@ namespace hass_workstation_service.Communication
{ {
return new MqqtClientStatus() { IsConnected = _mqttClient.IsConnected, Message = _mqttClientMessage }; return new MqqtClientStatus() { IsConnected = _mqttClient.IsConnected, Message = _mqttClientMessage };
} }
public async void AnnounceAvailability(string domain, bool offline = false)
{
if (this._mqttClient.IsConnected)
{
await this._mqttClient.PublishAsync(
new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{domain}/{DeviceConfigModel.Name}/availability")
.WithPayload(offline ? "offline" : "online")
.Build()
);
this.LastAvailabilityAnnounce = DateTime.UtcNow;
}
else
{
this._logger.LogInformation($"Availability announce dropped because mqtt not connected");
}
}
public async Task DisconnectAsync()
{
if (this._mqttClient.IsConnected)
{
await this._mqttClient.InternalClient.DisconnectAsync();
}
else
{
this._logger.LogInformation($"Disconnected");
}
}
public async void Subscribe(AbstractCommand command)
{
if (this.IsConnected)
{
await this._mqttClient.SubscribeAsync(command.GetAutoDiscoveryConfig().Command_topic);
}
else
{
while (this.IsConnected == false)
{
await Task.Delay(5500);
}
await this._mqttClient.SubscribeAsync(command.GetAutoDiscoveryConfig().Command_topic);
}
Subscribers.Add(command);
}
private void HandleMessageReceived(MqttApplicationMessage applicationMessage)
{
foreach (AbstractCommand command in this.Subscribers)
{
if (command.GetAutoDiscoveryConfig().Command_topic == applicationMessage.Topic)
{
if (Encoding.UTF8.GetString(applicationMessage?.Payload) == "ON")
{
command.TurnOn();
}
else if (Encoding.UTF8.GetString(applicationMessage?.Payload) == "OFF")
{
command.TurnOff();
}
}
}
}
} }
} }

@ -3,18 +3,26 @@ using System.Collections.Generic;
namespace hass_workstation_service.Communication namespace hass_workstation_service.Communication
{ {
public class AutoDiscoveryConfigModel public abstract class DiscoveryConfigModel
{ {
/// <summary> /// <summary>
/// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates. /// (Optional) Information about the device this sensor is a part of to tie it into the device registry. Only works through MQTT discovery and when unique_id is set.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public string Availability_topic { get; set; } public DeviceConfigModel Device { get; set; }
/// <summary> /// <summary>
/// (Optional) Information about the device this sensor is a part of to tie it into the device registry. Only works through MQTT discovery and when unique_id is set. /// (Optional) The name of the MQTT sensor. Defaults to MQTT Sensor in hass.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
public DeviceConfigModel Device { get; set; } public string Name { get; set; }
}
public class SensorDiscoveryConfigModel : DiscoveryConfigModel
{
/// <summary>
/// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates.
/// </summary>
/// <value></value>
public string Availability_topic { get; set; }
/// <summary> /// <summary>
/// (Optional) The type/class of the sensor to set the icon in the frontend. See https://www.home-assistant.io/integrations/sensor/#device-class for options. /// (Optional) The type/class of the sensor to set the icon in the frontend. See https://www.home-assistant.io/integrations/sensor/#device-class for options.
/// </summary> /// </summary>
@ -47,11 +55,6 @@ namespace hass_workstation_service.Communication
/// <value></value> /// <value></value>
public string Json_attributes_topic { get; set; } public string Json_attributes_topic { get; set; }
/// <summary> /// <summary>
/// (Optional) The name of the MQTT sensor. Defaults to MQTT Sensor in hass.
/// </summary>
/// <value></value>
public string Name { get; set; }
/// <summary>
/// (Optional) The payload that represents the available state. /// (Optional) The payload that represents the available state.
/// </summary> /// </summary>
/// <value></value> /// <value></value>
@ -89,6 +92,78 @@ namespace hass_workstation_service.Communication
public string Value_template { get; set; } public string Value_template { get; set; }
} }
public class CommandDiscoveryConfigModel : DiscoveryConfigModel
{
/// <summary>
/// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates.
/// </summary>
/// <value></value>
public string Availability_topic { get; set; }
/// <summary>
/// (Optional) The MQTT topic to set the command
/// </summary>
/// <value></value>
public string Command_topic { get; set; }
/// <summary>
/// (Optional) The type/class of the sensor to set the icon in the frontend. See https://www.home-assistant.io/integrations/sensor/#device-class for options.
/// </summary>
/// <value></value>
public string Device_class { get; set; }
/// <summary>
/// Sends update events even if the value hasnt changed. Useful if you want to have meaningful value graphs in history.
/// </summary>
/// <value></value>
public bool? Force_update { get; set; }
/// <summary>
/// (Optional) The icon for the sensor.
/// </summary>
/// <value></value>
public string Icon { get; set; }
/// <summary>
/// (Optional) Defines a template to extract the JSON dictionary from messages received on the json_attributes_topic.
/// </summary>
/// <value></value>
public string Json_attributes_template { get; set; }
/// <summary>
/// (Optional) The MQTT topic subscribed to receive a JSON dictionary payload and then set as sensor attributes. Implies force_update of the current sensor state when a message is received on this topic.
/// </summary>
/// <value></value>
public string Json_attributes_topic { get; set; }
/// <summary>
/// (Optional) The payload that represents the available state.
/// </summary>
/// <value></value>
public string Payload_available { get; set; }
/// <summary>
/// (Optional) The payload that represents the unavailable state.
/// </summary>
/// <value></value>
public string Payload_not_available { get; set; }
/// <summary>
/// (Optional) The maximum QoS level of the state topic.
/// </summary>
/// <value></value>
public int? Qos { get; set; }
/// <summary>
/// The MQTT topic subscribed to receive sensor values.
/// </summary>
/// <value></value>
public string State_topic { get; set; }
/// <summary>
/// (Optional) An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception.
/// </summary>
/// <value></value>
public string Unique_id { get; set; }
/// <summary>
/// (Optional) Defines a template to extract the value.
/// </summary>
/// <value></value>
public string Value_template { get; set; }
}
/// <summary> /// <summary>
/// This information will be used when announcing this device on the mqtt topic /// This information will be used when announcing this device on the mqtt topic
/// </summary> /// </summary>

@ -10,12 +10,14 @@ using System.Threading.Tasks;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
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.NamedPipe;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Win32; using Microsoft.Win32;
using MQTTnet; using MQTTnet;
using MQTTnet.Client; using MQTTnet.Client;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using MQTTnet.Extensions.ManagedClient;
using Serilog; using Serilog;
namespace hass_workstation_service.Data namespace hass_workstation_service.Data
@ -23,15 +25,19 @@ namespace hass_workstation_service.Data
public class ConfigurationService : IConfigurationService public class ConfigurationService : IConfigurationService
{ {
public ICollection<AbstractSensor> ConfiguredSensors { get; private set; } public ICollection<AbstractSensor> ConfiguredSensors { get; private set; }
public Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; } public ICollection<AbstractCommand> ConfiguredCommands { get; private set; }
public Action<IManagedMqttClientOptions> MqqtConfigChangedHandler { get; set; }
private readonly DeviceConfigModel _deviceConfigModel;
private bool BrokerSettingsFileLocked { get; set; } private bool BrokerSettingsFileLocked { get; set; }
private bool SensorsSettingsFileLocked { get; set; } private bool SensorsSettingsFileLocked { get; set; }
private bool CommandSettingsFileLocked { get; set; }
private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service"); private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service");
public ConfigurationService() public ConfigurationService(DeviceConfigModel deviceConfigModel)
{ {
this._deviceConfigModel = deviceConfigModel;
if (!File.Exists(Path.Combine(path, "mqttbroker.json"))) if (!File.Exists(Path.Combine(path, "mqttbroker.json")))
{ {
File.Create(Path.Combine(path, "mqttbroker.json")).Close(); File.Create(Path.Combine(path, "mqttbroker.json")).Close();
@ -42,7 +48,13 @@ namespace hass_workstation_service.Data
File.Create(Path.Combine(path, "configured-sensors.json")).Close(); File.Create(Path.Combine(path, "configured-sensors.json")).Close();
} }
if (!File.Exists(Path.Combine(path, "configured-commands.json")))
{
File.Create(Path.Combine(path, "configured-commands.json")).Close();
}
ConfiguredSensors = new List<AbstractSensor>(); ConfiguredSensors = new List<AbstractSensor>();
ConfiguredCommands = new List<AbstractCommand>();
} }
public async void ReadSensorSettings(MqttPublisher publisher) public async void ReadSensorSettings(MqttPublisher publisher)
@ -123,7 +135,54 @@ namespace hass_workstation_service.Data
} }
} }
public async Task<IMqttClientOptions> GetMqttClientOptionsAsync() public async void ReadCommandSettings(MqttPublisher publisher)
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> commands = new List<ConfiguredCommand>();
using (var stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
{
Log.Logger.Information($"reading configured commands from: {stream.Name}");
if (stream.Length > 0)
{
commands = await JsonSerializer.DeserializeAsync<List<ConfiguredCommand>>(stream);
}
stream.Close();
this.CommandSettingsFileLocked = false;
}
foreach (ConfiguredCommand configuredCommand in commands)
{
AbstractCommand command = null;
switch (configuredCommand.Type)
{
case "ShutdownCommand":
command = new ShutdownCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "RestartCommand":
command = new RestartCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "LogOffCommand":
command = new LogOffCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "CustomCommand":
command = new CustomCommand(publisher, configuredCommand.Command, configuredCommand.Name, configuredCommand.Id);
break;
default:
Log.Logger.Error("unsupported command type in config");
break;
}
if (command != null)
{
this.ConfiguredCommands.Add(command);
}
}
}
public async Task<IManagedMqttClientOptions> GetMqttClientOptionsAsync()
{ {
ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync(); ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync();
if (configuredBroker != null && configuredBroker.Host != null) if (configuredBroker != null && configuredBroker.Host != null)
@ -137,8 +196,14 @@ namespace hass_workstation_service.Data
AllowUntrustedCertificates = true AllowUntrustedCertificates = true
}) })
.WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString()) .WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString())
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
.WithWillMessage(new MqttApplicationMessageBuilder()
.WithRetainFlag()
.WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability")
.WithPayload("offline")
.Build())
.Build(); .Build();
return mqttClientOptions; return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build();
} }
else else
{ {
@ -173,7 +238,7 @@ namespace hass_workstation_service.Data
return configuredBroker; return configuredBroker;
} }
public async void WriteSettingsAsync() public async void WriteSensorSettingsAsync()
{ {
while (this.SensorsSettingsFileLocked) while (this.SensorsSettingsFileLocked)
{ {
@ -210,11 +275,44 @@ namespace hass_workstation_service.Data
this.SensorsSettingsFileLocked = false; this.SensorsSettingsFileLocked = false;
} }
public async void WriteCommandSettingsAsync()
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> configuredCommandsToSave = new List<ConfiguredCommand>();
using (FileStream stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured commands to: {stream.Name}");
foreach (AbstractCommand command in this.ConfiguredCommands)
{
if (command is CustomCommand customcommand)
{
configuredCommandsToSave.Add(new ConfiguredCommand() { Id = customcommand.Id, Name = customcommand.Name, Type = customcommand.GetType().Name, Command = customcommand.Command });
}
}
await JsonSerializer.SerializeAsync(stream, configuredCommandsToSave);
stream.Close();
}
this.CommandSettingsFileLocked = false;
}
public void AddConfiguredSensor(AbstractSensor sensor) public void AddConfiguredSensor(AbstractSensor sensor)
{ {
this.ConfiguredSensors.Add(sensor); this.ConfiguredSensors.Add(sensor);
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
WriteSettingsAsync(); WriteSensorSettingsAsync();
}
public void AddConfiguredCommand(AbstractCommand command)
{
this.ConfiguredCommands.Add(command);
command.PublishAutoDiscoveryConfigAsync();
WriteCommandSettingsAsync();
} }
public async void DeleteConfiguredSensor(Guid id) public async void DeleteConfiguredSensor(Guid id)
@ -224,7 +322,7 @@ namespace hass_workstation_service.Data
{ {
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync(); await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
this.ConfiguredSensors.Remove(sensorToRemove); this.ConfiguredSensors.Remove(sensorToRemove);
WriteSettingsAsync(); WriteSensorSettingsAsync();
} }
else else
{ {
@ -233,10 +331,26 @@ namespace hass_workstation_service.Data
} }
public async void DeleteConfiguredCommand(Guid id)
{
var commandToRemove = this.ConfiguredCommands.FirstOrDefault(s => s.Id == id);
if (commandToRemove != null)
{
await commandToRemove.UnPublishAutoDiscoveryConfigAsync();
this.ConfiguredCommands.Remove(commandToRemove);
WriteCommandSettingsAsync();
}
else
{
Log.Logger.Warning($"command with id {id} not found");
}
}
public void AddConfiguredSensors(List<AbstractSensor> sensors) public void AddConfiguredSensors(List<AbstractSensor> sensors)
{ {
sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor)); sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor));
WriteSettingsAsync(); WriteSensorSettingsAsync();
} }
/// <summary> /// <summary>

@ -0,0 +1,13 @@
using hass_workstation_service.Domain.Sensors;
using System;
namespace hass_workstation_service.Data
{
public class ConfiguredCommand
{
public string Type { get; set; }
public Guid Id { get; set; }
public string Name { get; set; }
public string Command { get; set; }
}
}

@ -1,7 +1,9 @@
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using MQTTnet.Extensions.ManagedClient;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Security; using System.Security;
@ -12,17 +14,22 @@ namespace hass_workstation_service.Data
public interface IConfigurationService public interface IConfigurationService
{ {
ICollection<AbstractSensor> ConfiguredSensors { get; } ICollection<AbstractSensor> ConfiguredSensors { get; }
Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; } Action<IManagedMqttClientOptions> MqqtConfigChangedHandler { get; set; }
ICollection<AbstractCommand> ConfiguredCommands { get; }
void AddConfiguredCommand(AbstractCommand command);
void AddConfiguredSensor(AbstractSensor sensor); void AddConfiguredSensor(AbstractSensor sensor);
void AddConfiguredSensors(List<AbstractSensor> sensors); void AddConfiguredSensors(List<AbstractSensor> sensors);
Task<IMqttClientOptions> GetMqttClientOptionsAsync(); Task<IManagedMqttClientOptions> GetMqttClientOptionsAsync();
void ReadSensorSettings(MqttPublisher publisher); void ReadSensorSettings(MqttPublisher publisher);
void WriteMqttBrokerSettingsAsync(MqttSettings settings); void WriteMqttBrokerSettingsAsync(MqttSettings settings);
void WriteSettingsAsync(); void WriteSensorSettingsAsync();
Task<MqttSettings> GetMqttBrokerSettings(); Task<MqttSettings> GetMqttBrokerSettings();
void EnableAutoStart(bool enable); void EnableAutoStart(bool enable);
bool IsAutoStartEnabled(); bool IsAutoStartEnabled();
void DeleteConfiguredSensor(Guid id); void DeleteConfiguredSensor(Guid id);
void DeleteConfiguredCommand(Guid id);
void WriteCommandSettingsAsync();
void ReadCommandSettings(MqttPublisher publisher);
} }
} }

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Domain
{
public abstract class AbstractDiscoverable
{
public abstract string Domain { get; }
}
}

@ -0,0 +1,80 @@
using System;
using System.Threading.Tasks;
using hass_workstation_service.Communication;
using MQTTnet;
namespace hass_workstation_service.Domain.Commands
{
public abstract class AbstractCommand : AbstractDiscoverable
{
public Guid Id { get; protected set; }
public string Name { get; protected set; }
/// <summary>
/// The update interval in seconds. It checks state only if the interval has passed.
/// </summary>
public int UpdateInterval { get => 1; }
public DateTime? LastUpdated { get; protected set; }
public string PreviousPublishedState { get; protected set; }
public MqttPublisher Publisher { get; protected set; }
public override string Domain { get => "switch"; }
public AbstractCommand(MqttPublisher publisher, string name, Guid id = default(Guid))
{
if (id == Guid.Empty)
{
this.Id = Guid.NewGuid();
}
else
{
this.Id = id;
}
this.Name = name;
this.Publisher = publisher;
publisher.Subscribe(this);
}
protected CommandDiscoveryConfigModel _autoDiscoveryConfigModel;
protected CommandDiscoveryConfigModel SetAutoDiscoveryConfigModel(CommandDiscoveryConfigModel config)
{
this._autoDiscoveryConfigModel = config;
return config;
}
public abstract CommandDiscoveryConfigModel GetAutoDiscoveryConfig();
public abstract string GetState();
public async Task PublishStateAsync()
{
if (LastUpdated.HasValue && LastUpdated.Value.AddSeconds(this.UpdateInterval) > DateTime.UtcNow)
{
// dont't even check the state if the update interval hasn't passed
return;
}
string state = this.GetState();
if (this.PreviousPublishedState == state)
{
// don't publish the state if it hasn't changed
return;
}
var message = new MqttApplicationMessageBuilder()
.WithTopic(this.GetAutoDiscoveryConfig().State_topic)
.WithPayload(state)
.WithExactlyOnceQoS()
.WithRetainFlag()
.Build();
await Publisher.Publish(message);
this.PreviousPublishedState = state;
this.LastUpdated = DateTime.UtcNow;
}
public async void PublishAutoDiscoveryConfigAsync()
{
await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain);
}
public async Task UnPublishAutoDiscoveryConfigAsync()
{
await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain, true);
}
public abstract void TurnOn();
public abstract void TurnOff();
}
}

@ -0,0 +1,75 @@
using hass_workstation_service.Communication;
using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class CustomCommand : AbstractCommand
{
public string Command { get; protected set; }
public string State { get; protected set; }
public Process Process { get; private set; }
public CustomCommand(MqttPublisher publisher, string command, string name = "Custom", Guid id = default(Guid)) : base(publisher, name ?? "Custom", id)
{
this.Command = command;
this.State = "OFF";
}
public override async void TurnOn()
{
this.State = "ON";
this.Process = new Process();
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.CreateNoWindow = true;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = $"/C {this.Command}";
this.Process.StartInfo = startInfo;
try
{
this.Process.Start();
}
catch (Exception e)
{
Log.Logger.Error($"Sensor {this.Name} failed", e);
this.State = "FAILED";
}
while (!this.Process.HasExited)
{
await Task.Delay(1000);
}
this.State = "OFF";
}
public override CommandDiscoveryConfigModel GetAutoDiscoveryConfig()
{
return new CommandDiscoveryConfigModel()
{
Name = this.Name,
Unique_id = this.Id.ToString(),
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/set",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Device = this.Publisher.DeviceConfigModel,
};
}
public override string GetState()
{
return this.State;
}
public override void TurnOff()
{
this.Process.Kill();
}
}
}

@ -0,0 +1,17 @@
using hass_workstation_service.Communication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class LogOffCommand : CustomCommand
{
public LogOffCommand(MqttPublisher publisher, string name = "Shutdown", Guid id = default(Guid)) : base(publisher, "shutdown /l", name ?? "LogOff", id)
{
this.State = "OFF";
}
}
}

@ -0,0 +1,17 @@
using hass_workstation_service.Communication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class RestartCommand : CustomCommand
{
public RestartCommand(MqttPublisher publisher, string name = "Shutdown", Guid id = default(Guid)) : base(publisher, "shutdown /r", name ?? "Restart", id)
{
this.State = "OFF";
}
}
}

@ -0,0 +1,17 @@
using hass_workstation_service.Communication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class ShutdownCommand : CustomCommand
{
public ShutdownCommand(MqttPublisher publisher, string name = "Shutdown", Guid id = default(Guid)) : base(publisher, "shutdown /s", name ?? "Shutdown", id)
{
this.State = "OFF";
}
}
}

@ -6,7 +6,7 @@ using MQTTnet;
namespace hass_workstation_service.Domain.Sensors namespace hass_workstation_service.Domain.Sensors
{ {
public abstract class AbstractSensor public abstract class AbstractSensor : AbstractDiscoverable
{ {
public Guid Id { get; protected set; } public Guid Id { get; protected set; }
public string Name { get; protected set; } public string Name { get; protected set; }
@ -17,6 +17,7 @@ namespace hass_workstation_service.Domain.Sensors
public DateTime? LastUpdated { get; protected set; } public DateTime? LastUpdated { get; protected set; }
public string PreviousPublishedState { get; protected set; } public string PreviousPublishedState { get; protected set; }
public MqttPublisher Publisher { get; protected set; } public MqttPublisher Publisher { get; protected set; }
public override string Domain { get => "sensor"; }
public AbstractSensor(MqttPublisher publisher, string name, int updateInterval = 10, Guid id = default(Guid)) public AbstractSensor(MqttPublisher publisher, string name, int updateInterval = 10, Guid id = default(Guid))
{ {
if (id == Guid.Empty) if (id == Guid.Empty)
@ -32,14 +33,14 @@ namespace hass_workstation_service.Domain.Sensors
this.UpdateInterval = updateInterval; this.UpdateInterval = updateInterval;
} }
protected AutoDiscoveryConfigModel _autoDiscoveryConfigModel; protected SensorDiscoveryConfigModel _autoDiscoveryConfigModel;
protected AutoDiscoveryConfigModel SetAutoDiscoveryConfigModel(AutoDiscoveryConfigModel config) protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config)
{ {
this._autoDiscoveryConfigModel = config; this._autoDiscoveryConfigModel = config;
return config; return config;
} }
public abstract AutoDiscoveryConfigModel GetAutoDiscoveryConfig(); public abstract SensorDiscoveryConfigModel GetAutoDiscoveryConfig();
public abstract string GetState(); public abstract string GetState();
public async Task PublishStateAsync() public async Task PublishStateAsync()
@ -67,11 +68,11 @@ namespace hass_workstation_service.Domain.Sensors
} }
public async void PublishAutoDiscoveryConfigAsync() public async void PublishAutoDiscoveryConfigAsync()
{ {
await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig()); await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain);
} }
public async Task UnPublishAutoDiscoveryConfigAsync() public async Task UnPublishAutoDiscoveryConfigAsync()
{ {
await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), true); await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain, true);
} }
} }

@ -9,15 +9,16 @@ namespace hass_workstation_service.Domain.Sensors
public class ActiveWindowSensor : AbstractSensor public class ActiveWindowSensor : AbstractSensor
{ {
public ActiveWindowSensor(MqttPublisher publisher, int? updateInterval = null, string name = "ActiveWindow", Guid id = default(Guid)) : base(publisher, name ?? "ActiveWindow", updateInterval ?? 10, id) { } public ActiveWindowSensor(MqttPublisher publisher, int? updateInterval = null, string name = "ActiveWindow", Guid id = default(Guid)) : base(publisher, name ?? "ActiveWindow", updateInterval ?? 10, id) { }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:window-maximize", Icon = "mdi:window-maximize",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -17,16 +17,17 @@ namespace hass_workstation_service.Domain.Sensors
{ {
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:chart-areaspline", Icon = "mdi:chart-areaspline",
Unit_of_measurement = "%" Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -9,16 +9,17 @@ namespace hass_workstation_service.Domain.Sensors
{ {
public CurrentClockSpeedSensor(MqttPublisher publisher, int? updateInterval = null, string name = "CurrentClockSpeed", Guid id = default(Guid)) : base(publisher, "SELECT CurrentClockSpeed FROM Win32_Processor", updateInterval ?? 10, name ?? "CurrentClockSpeed", id) { } public CurrentClockSpeedSensor(MqttPublisher publisher, int? updateInterval = null, string name = "CurrentClockSpeed", Guid id = default(Guid)) : base(publisher, "SELECT CurrentClockSpeed FROM Win32_Processor", updateInterval ?? 10, name ?? "CurrentClockSpeed", id) { }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:speedometer", Icon = "mdi:speedometer",
Unit_of_measurement = "MHz" Unit_of_measurement = "MHz",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }
} }

@ -13,14 +13,15 @@ namespace hass_workstation_service.Domain.Sensors
this._random = new Random(); this._random = new Random();
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state" State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -9,15 +9,16 @@ namespace hass_workstation_service.Domain.Sensors
public LastActiveSensor(MqttPublisher publisher, int? updateInterval = 10, string name = "LastActive", Guid id = default) : base(publisher, name ?? "LastActive", updateInterval ?? 10, id){} public LastActiveSensor(MqttPublisher publisher, int? updateInterval = 10, string name = "LastActive", Guid id = default) : base(publisher, name ?? "LastActive", updateInterval ?? 10, id){}
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:clock-time-three-outline" Icon = "mdi:clock-time-three-outline",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -13,15 +13,16 @@ namespace hass_workstation_service.Domain.Sensors
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:clock-time-three-outline" Icon = "mdi:clock-time-three-outline",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -34,16 +34,17 @@ namespace hass_workstation_service.Domain.Sensors
} }
return ""; return "";
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:memory", Icon = "mdi:memory",
Unit_of_measurement = "%" Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }
} }

@ -21,33 +21,100 @@ namespace hass_workstation_service.Domain.Sensors
} }
else return "unsupported"; else return "unsupported";
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:microphone", Icon = "mdi:microphone",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private bool IsMicrophoneInUse() private bool IsMicrophoneInUse()
{ {
using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone\NonPackaged")) using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone"))
{ {
foreach (var subKeyName in key.GetSubKeyNames()) foreach (var subKeyName in key.GetSubKeyNames())
{ {
using (var subKey = key.OpenSubKey(subKeyName)) // NonPackaged has multiple subkeys
if (subKeyName == "NonPackaged")
{ {
if (subKey.GetValueNames().Contains("LastUsedTimeStop")) using (var nonpackagedkey = key.OpenSubKey(subKeyName))
{ {
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames())
if (endTime <= 0)
{ {
return true; using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
else
{
using (var subKey = key.OpenSubKey(subKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\microphone"))
{
foreach (var subKeyName in key.GetSubKeyNames())
{
// NonPackaged has multiple subkeys
if (subKeyName == "NonPackaged")
{
using (var nonpackagedkey = key.OpenSubKey(subKeyName))
{
foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames())
{
using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
else
{
using (var subKey = key.OpenSubKey(subKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
} }
} }
} }

@ -16,15 +16,16 @@ namespace hass_workstation_service.Domain.Sensors
this.WindowName = windowName; this.WindowName = windowName;
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:window-maximize", Icon = "mdi:window-maximize",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -35,15 +35,16 @@ namespace hass_workstation_service.Domain.Sensors
public class SessionStateSensor : AbstractSensor 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 SessionStateSensor(MqttPublisher publisher, int? updateInterval = null, string name = "SessionState", Guid id = default(Guid)) : base(publisher, name ?? "SessionState", updateInterval ?? 10, id) { }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:lock", Icon = "mdi:lock",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -9,15 +9,16 @@ namespace hass_workstation_service.Domain.Sensors
{ {
public UserNotificationStateSensor(MqttPublisher publisher, int? updateInterval = null, string name = "NotificationState", Guid id = default(Guid)) : base(publisher, name ?? "NotificationState", updateInterval ?? 10, id) { } public UserNotificationStateSensor(MqttPublisher publisher, int? updateInterval = null, string name = "NotificationState", Guid id = default(Guid)) : base(publisher, name ?? "NotificationState", updateInterval ?? 10, id) { }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:laptop", Icon = "mdi:laptop",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -20,14 +20,15 @@ namespace hass_workstation_service.Domain.Sensors
_objectQuery = new ObjectQuery(this.Query); _objectQuery = new ObjectQuery(this.Query);
_searcher = new ManagementObjectSearcher(query); _searcher = new ManagementObjectSearcher(query);
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }

@ -23,33 +23,100 @@ namespace hass_workstation_service.Domain.Sensors
return "unsupported"; return "unsupported";
} }
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:webcam", Icon = "mdi:webcam",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
}); });
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
private bool IsWebCamInUseRegistry() private bool IsWebCamInUseRegistry()
{ {
using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam\NonPackaged")) using (var key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam"))
{ {
foreach (var subKeyName in key.GetSubKeyNames()) foreach (var subKeyName in key.GetSubKeyNames())
{ {
using (var subKey = key.OpenSubKey(subKeyName)) // NonPackaged has multiple subkeys
if (subKeyName == "NonPackaged")
{ {
if (subKey.GetValueNames().Contains("LastUsedTimeStop")) using (var nonpackagedkey = key.OpenSubKey(subKeyName))
{ {
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1; foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames())
if (endTime <= 0)
{ {
return true; using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
else
{
using (var subKey = key.OpenSubKey(subKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
using (var key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore\webcam"))
{
foreach (var subKeyName in key.GetSubKeyNames())
{
// NonPackaged has multiple subkeys
if (subKeyName == "NonPackaged")
{
using (var nonpackagedkey = key.OpenSubKey(subKeyName))
{
foreach (var nonpackagedSubKeyName in nonpackagedkey.GetSubKeyNames())
{
using (var subKey = nonpackagedkey.OpenSubKey(nonpackagedSubKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
}
}
}
}
}
else
{
using (var subKey = key.OpenSubKey(subKeyName))
{
if (subKey.GetValueNames().Contains("LastUsedTimeStop"))
{
var endTime = subKey.GetValue("LastUsedTimeStop") is long ? (long)subKey.GetValue("LastUsedTimeStop") : -1;
if (endTime <= 0)
{
return true;
}
} }
} }
} }

@ -67,11 +67,10 @@ namespace hass_workstation_service
Log.CloseAndFlush(); Log.CloseAndFlush();
} }
} }
} }
public static IHostBuilder CreateHostBuilder(string[] args) => public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args) Host.CreateDefaultBuilder(args)
.ConfigureLogging((hostContext, loggingBuilder) => .ConfigureLogging((hostContext, loggingBuilder) =>
loggingBuilder.AddSerilog(dispose: true)) loggingBuilder.AddSerilog(dispose: true))
.ConfigureServices((hostContext, services) => .ConfigureServices((hostContext, services) =>

@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
--> -->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<ApplicationRevision>27</ApplicationRevision> <ApplicationRevision>32</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion> <ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled> <BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration> <Configuration>Release</Configuration>

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Data; using hass_workstation_service.Data;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -30,6 +31,7 @@ namespace hass_workstation_service
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_configurationService.ReadCommandSettings(_mqttPublisher);
_configurationService.ReadSensorSettings(_mqttPublisher); _configurationService.ReadSensorSettings(_mqttPublisher);
while (!_mqttPublisher.IsConnected) while (!_mqttPublisher.IsConnected)
@ -40,16 +42,26 @@ namespace hass_workstation_service
_logger.LogInformation("Connected. Sending auto discovery messages."); _logger.LogInformation("Connected. Sending auto discovery messages.");
List<AbstractSensor> sensors = _configurationService.ConfiguredSensors.ToList(); List<AbstractSensor> sensors = _configurationService.ConfiguredSensors.ToList();
List<AbstractCommand> commands = _configurationService.ConfiguredCommands.ToList();
_mqttPublisher.AnnounceAvailability("sensor");
foreach (AbstractSensor sensor in sensors) foreach (AbstractSensor sensor in sensors)
{ {
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
} }
foreach (AbstractCommand command in commands)
{
command.PublishAutoDiscoveryConfigAsync();
}
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
sensors = _configurationService.ConfiguredSensors.ToList();
_logger.LogDebug("Worker running at: {time}", DateTimeOffset.Now); _logger.LogDebug("Worker running at: {time}", DateTimeOffset.Now);
// announce autodiscovery every 30 seconds
if (_mqttPublisher.LastAvailabilityAnnounce < DateTime.UtcNow.AddSeconds(-10))
{
_mqttPublisher.AnnounceAvailability("sensor");
}
foreach (AbstractSensor sensor in sensors) foreach (AbstractSensor sensor in sensors)
{ {
try try
@ -60,7 +72,19 @@ namespace hass_workstation_service
{ {
Log.Logger.Warning("Sensor failed: " + sensor.Name, ex); Log.Logger.Warning("Sensor failed: " + sensor.Name, ex);
} }
}
foreach (AbstractCommand command in commands)
{
try
{
await command.PublishStateAsync();
}
catch (Exception ex)
{
Log.Logger.Warning("Command state failed: " + command.Name, ex);
}
} }
// announce autodiscovery every 30 seconds // announce autodiscovery every 30 seconds
if (_mqttPublisher.LastConfigAnnounce < DateTime.UtcNow.AddSeconds(-30)) if (_mqttPublisher.LastConfigAnnounce < DateTime.UtcNow.AddSeconds(-30))
@ -69,9 +93,21 @@ namespace hass_workstation_service
{ {
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
} }
foreach (AbstractCommand command in commands)
{
command.PublishAutoDiscoveryConfigAsync();
}
} }
await Task.Delay(1000, stoppingToken); await Task.Delay(1000, stoppingToken);
} }
}
public override async Task StopAsync(CancellationToken stoppingToken)
{
_mqttPublisher.AnnounceAvailability("sensor", true);
await _mqttPublisher.DisconnectAsync();
} }
} }
} }

@ -49,6 +49,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="MQTTnet" Version="3.0.13" /> <PackageReference Include="MQTTnet" Version="3.0.13" />
<PackageReference Include="MQTTnet.Extensions.ManagedClient" Version="3.0.13" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" /> <PackageReference Include="Serilog.Extensions.Logging.File" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> <PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" /> <PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />

Loading…
Cancel
Save