Merge branch 'develop'

pull/130/head 1.0.0.31612
Sleevezipper 3 years ago
commit 242e72601f

@ -24,7 +24,7 @@ Not convinced yet? Check out [this excellent video](https://youtu.be/D5A7le79R5M
## 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. 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.
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 get an error stating your system's settings not allowing installation, please refer to [this StackOverflow answer](https://superuser.com/a/1252757).
### Standalone
@ -47,14 +47,14 @@ Find us on [Discord](https://discord.gg/VraYT2N3wd), or check out the [frequentl
## Development
Want to develop or build the application yourself? Make sure to install the .NET Runtime [.NET 5 Runtime](https://dotnet.microsoft.com/download/dotnet/current/runtime) and [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/current). Run the following commands from the `hass-workstation-service\hass-workstation-service` directory to get you started:
If you want to help develop Hass Workstation service, make sure to install the .NET Runtime [.NET 5 Runtime](https://dotnet.microsoft.com/download/dotnet/current/runtime) and [.NET 5 SDK](https://dotnet.microsoft.com/download/dotnet/current). Run the following commands from the `hass-workstation-service\hass-workstation-service` directory to get you started:
```` powershell
dotnet build
dotnet publish
````
In case you are using Visual Studio Code, open the `hass-workstation-service\hass-workstation-service` folder to take advantage of the predefined build and publish tasks.
If you are using [Visual Studio](https://visualstudio.microsoft.com/), open the `hass-workstation-service\hass-workstation-service` folder to take advantage of the predefined build and publish tasks, alternatively you can open the project directly from github using the green download button to use the integrated git tools.
## Sensors
@ -62,151 +62,25 @@ The application provides several sensors. Sensors can be configured with a name
Sensors publish their state on their own interval which you can configure and only publish when the state changes.
### UserNotificationState
Here is a list of the most commonly used sensors with the full documentation [here](https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md):
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. Notice that this status does not watch Focus Assist. It has the following possible states:
|State|Explanation|
|---|---|
|NotPresent|A screen saver is displayed, the machine is locked, or a nonactive Fast User Switching session is in progress. |
|Busy|A full-screen application is running or Presentation Settings are applied. Presentation Settings allow a user to put their machine into a state fit for an uninterrupted presentation, such as a set of PowerPoint slides, with a single click.|
|RunningDirect3dFullScreen|A full-screen (exclusive mode) Direct3D application is running.|
|PresentationMode|The user has activated Windows presentation settings to block notifications and pop-up messages.|
|AcceptsNotifications|None of the other states are found, notifications can be freely sent.|
|QuietTime|Introduced in Windows 7. The current user is in "quiet time", which is the first hour after a new user logs into his or her account for the first time. During this time, most notifications should not be sent or shown. This lets a user become accustomed to a new computer system without those distractions. Quiet time also occurs for each user after an operating system upgrade or clean installation.|
|RunningWindowsStoreApp|A Windows Store app is running.|
### ActiveWindow
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.
### GPULoad
This sensor returns the current GPU load. This should work for both NVidia and AMD GPU's.
### GPUTemperature
This sensor returns the current temperature of the GPU in °C. This should work for both NVidia and AMD GPU's.
### UsedMemory
This sensor calculates the percentage of used memory.
### CurrentClockSpeed
This sensor returns the BIOS configured baseclock for the processor.
### WMIQuery
This advanced sensor executes a user defined [WMI query](https://docs.microsoft.com/en-us/windows/win32/wmisdk/wmi-and-sql) and exposes the result. The query should return a single value.
For example:
```sql
SELECT * FROM Win32_Processor
```
returns
`|64|9|To Be Filled By O.E.M.|3|Intel64 Family 6 Model 94 Stepping 3|252|1|Win32_Processor|4008|12|64|Intel64 Family 6 Model 94 Stepping 3|CPU0|100|198|1024|8192|0|6|4|GenuineIntel|4008|Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz|4|4|8|To Be Filled By O.E.M.|False|BFEBFBFF000506E3|3|24067|CPU|False|To Be Filled By O.E.M.|U3E1|OK|3|Win32_ComputerSystem|GAME-PC-2016|8|1|False|False|`
This cannot not be used for this sensor. Instead try
```sql
SELECT CurrentClockSpeed FROM Win32_Processor
```
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.
Here's some queries from other users:
|Query|Explanation|Thanks|
|---|---|---|
|`SELECT username FROM Win32_ComputerSystem`|Shows the current user|@grizzlyjere|
Want to add you query here? Please create a pull request or open an issue.
### LastActive
This sensor returns the date/time that the workstation was last active. Typing or moving your mouse will reset the date/time.
### LastBoot
This sensor returns the date/time that Windows was last booted.
### SessionState
This sensor returns the current session state. It has the following possible states:
|State|Explanation|
|sensor|use|
|---|---|
|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.|
### CurrentVolume
This sensor returns the volume of the currently playing audio. So if you're listening to music and you pause, this sensor will return 0 (or at least a very low value).
### Dummy
This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.
|ActiveWindow|Exposes the currently selected window|
|WebcamActive|Exposes the microphone state|
|MicrophoneActive|Exposes the webcam state|
## 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 the 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`.
This application allows you to send commands over MQTT to control the host system, and will be exposed using [MQTT discovery](https://www.home-assistant.io/docs/mqtt/discovery/). Alternatively you can directly send a command from Home Assistant using this topic : `homeassistant/switch/{DeviceName}/{Name}/set`, with the payload `ON`.
### CustomCommand
Here is a list of the most commonly used sensors with the full documentation [here](https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md)
This command allows you to run any Windows Commands. The command will be run in a hidden Command Prompt. Some examples:
|Command|Explanation|
|command|use|
|---|---|
|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.|
### KeyCommand
Sends a keystroke with the specified key. You can pick [any of these](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) key codes.
### Media Commands
There's several media commands available which are very self exlanatory.
- Play/Pause
- Next
- Previous
- Volume up
- Volume down
- Mute (toggle)
|ShutdownCommand|Shutdown the PC|
|RestartCommand|Restart the PC|
|MuteCommand|Mute the speakers|
## Credits

@ -5,20 +5,19 @@
<StartupObject>UserInterface.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
<Folder Include="Models\" />
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\hass-workstation-logo.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.0" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.0" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.0" />
<PackageReference Include="Avalonia.Win32" Version="0.10.0" />
<PackageReference Include="Avalonia" Version="0.10.8" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.8" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.8" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.8" />
<PackageReference Include="Avalonia.Win32" Version="0.10.8" />
<PackageReference Include="JKang.IpcServiceFramework.Client.NamedPipe" Version="3.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\hass-workstation-service\hass-workstation-service.csproj" />
@ -33,6 +32,9 @@
<Compile Update="Views\BackgroundServiceSettings.axaml.cs">
<DependentUpon>BackgroundServiceSettings.axaml</DependentUpon>
</Compile>
<Compile Update="Views\GeneralSettings.axaml.cs">
<DependentUpon>GeneralSettings.axaml</DependentUpon>
</Compile>
<Compile Update="Views\CommandSettings.axaml.cs">
<DependentUpon>CommandSettings.axaml</DependentUpon>
</Compile>

@ -1,34 +1,26 @@
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;
private AvailableCommands _selectedType;
private string _name;
private string _description;
private bool _showCommandInput;
private bool _showKeyInput;
private string _moreInfoLink;
private string _command;
private string _key;
public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); }
public bool ShowCommandInput { get => showCommandInput; set => this.RaiseAndSetIfChanged(ref showCommandInput, value); }
public bool ShowKeyInput { get => showKeyInput; set => this.RaiseAndSetIfChanged(ref showKeyInput, value); }
private string moreInfoLink;
private bool showCommandInput;
private bool showKeyInput;
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; }
public string Key { get; set; }
public AvailableCommands SelectedType { get => _selectedType; set => this.RaiseAndSetIfChanged(ref _selectedType, value); }
public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); }
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
public bool ShowCommandInput { get => _showCommandInput; set => this.RaiseAndSetIfChanged(ref _showCommandInput, value); }
public bool ShowKeyInput { get => _showKeyInput; set => this.RaiseAndSetIfChanged(ref _showKeyInput, value); }
public string MoreInfoLink { get => _moreInfoLink; set => this.RaiseAndSetIfChanged(ref _moreInfoLink, value); }
public string Command { get => _command; set => this.RaiseAndSetIfChanged(ref _command, value); }
public string Key { get => _key; set => this.RaiseAndSetIfChanged(ref _key, value); }
}
}
}

@ -1,40 +1,28 @@
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class AddSensorViewModel : ViewModelBase
{
private AvailableSensors selectedType;
private string description;
private bool showQueryInput;
public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); }
public bool ShowQueryInput { get => showQueryInput; set => this.RaiseAndSetIfChanged(ref showQueryInput, value); }
public bool 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
{
get { return moreInfoLink; }
set { this.RaiseAndSetIfChanged(ref moreInfoLink, value); }
}
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; }
public int UpdateInterval { get => updateInterval; set => this.RaiseAndSetIfChanged(ref updateInterval, value); }
private AvailableSensors _selectedType;
private string _name;
private int _updateInterval;
private string _description;
private bool _showQueryInput;
private bool _showWindowNameInput;
private string _moreInfoLink;
private string _query;
private string _windowName;
public AvailableSensors SelectedType { get => _selectedType; set => this.RaiseAndSetIfChanged(ref _selectedType, value); }
public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); }
public int UpdateInterval { get => _updateInterval; set => this.RaiseAndSetIfChanged(ref _updateInterval, value); }
public string Description { get => _description; set => this.RaiseAndSetIfChanged(ref _description, value); }
public bool ShowQueryInput { get => _showQueryInput; set => this.RaiseAndSetIfChanged(ref _showQueryInput, value); }
public bool ShowWindowNameInput { get => _showWindowNameInput; set => this.RaiseAndSetIfChanged(ref _showWindowNameInput, value); }
public string MoreInfoLink { get => _moreInfoLink; set => this.RaiseAndSetIfChanged(ref _moreInfoLink, value); }
public string Query { get => _query; set => this.RaiseAndSetIfChanged(ref _query, value); }
public string WindowName { get => _windowName; set => this.RaiseAndSetIfChanged(ref _windowName, value); }
}
}
}

@ -1,15 +1,20 @@
using ReactiveUI;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class CommandSettingsViewModel : ViewModelBase
{
private ICollection<CommandViewModel> configuredCommands;
private ICollection<CommandViewModel> _configuredCommands;
public ICollection<CommandViewModel> ConfiguredCommands
{
get => _configuredCommands;
set => this.RaiseAndSetIfChanged(ref _configuredCommands, value);
}
public ICollection<CommandViewModel> ConfiguredCommands { get => configuredCommands; set => this.RaiseAndSetIfChanged(ref configuredCommands, value); }
public void TriggerUpdate()
{
this.RaisePropertyChanged();
@ -19,7 +24,7 @@ namespace UserInterface.ViewModels
public class CommandViewModel : ViewModelBase
{
public Guid Id { get; set; }
public string Type { get; set; }
public AvailableCommands Type { get; set; }
public string Name { get; set; }
}
}
}

@ -0,0 +1,22 @@
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Data;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
namespace UserInterface.ViewModels
{
public class GeneralSettingsViewModel : ViewModelBase
{
private string namePrefix;
public string NamePrefix { get => namePrefix; set => this.RaiseAndSetIfChanged(ref namePrefix, value); }
public void Update(GeneralSettings settings)
{
this.NamePrefix = settings.NamePrefix;
}
}
}

@ -1,15 +1,20 @@
using ReactiveUI;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Text;
namespace UserInterface.ViewModels
{
public class SensorSettingsViewModel : ViewModelBase
{
private ICollection<SensorViewModel> configuredSensors;
private ICollection<SensorViewModel> _configuredSensors;
public ICollection<SensorViewModel> ConfiguredSensors
{
get => _configuredSensors;
set => this.RaiseAndSetIfChanged(ref _configuredSensors, value);
}
public ICollection<SensorViewModel> ConfiguredSensors { get => configuredSensors; set => this.RaiseAndSetIfChanged(ref configuredSensors, value); }
public void TriggerUpdate()
{
this.RaisePropertyChanged();
@ -21,30 +26,21 @@ namespace UserInterface.ViewModels
private string _value;
public Guid Id { get; set; }
public string Type { get; set; }
public AvailableSensors Type { get; set; }
public string Name { get; set; }
public int UpdateInterval { get; set; }
public string Value
{
get => _value; set
get => _value;
set
{
this.RaiseAndSetIfChanged(ref _value, value);
this.RaisePropertyChanged("ValueString");
this.RaisePropertyChanged(nameof(ValueString));
}
}
public string UnitOfMeasurement { get; set; }
public string ValueString
{
get
{
if (!string.IsNullOrWhiteSpace(_value))
{
return _value + " " + UnitOfMeasurement;
}
else return "";
}
}
public string ValueString => string.IsNullOrWhiteSpace(_value) ? string.Empty : $"{_value} {UnitOfMeasurement}";
}
}
}

@ -5,24 +5,26 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="UserInterface.Views.AddCommandDialog"
SizeToContent="WidthAndHeight"
Title="Add command">
<StackPanel Margin="40" MinWidth="200">
<ContentControl Margin="0 20 0 10">Command type</ContentControl>
Title="Add / edit command">
<StackPanel Margin="40" MinWidth="200">
<ContentControl Margin="0 20 0 10">Command 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"/>
<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"/>
<ContentControl IsVisible="{Binding ShowKeyInput}" Margin="0 20 0 10">Key</ContentControl>
<TextBox IsVisible="{Binding ShowKeyInput}" Text="{Binding Key}" Watermark="0xAD" 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>
<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"/>
<ContentControl IsVisible="{Binding ShowKeyInput}" Margin="0 20 0 10">Key</ContentControl>
<TextBox IsVisible="{Binding ShowKeyInput}" Text="{Binding Key}" Watermark="0xAD" HorizontalAlignment="Left" MinWidth="300"/>
<Grid>
<Button IsVisible="{Binding ShowCommandInput}" Width="75" HorizontalAlignment="Left" Margin="0 40 0 10" Click="Test">Test</Button>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Save">Save</Button>
</Grid>
</StackPanel>
</Window>

@ -7,7 +7,6 @@ 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;
@ -17,107 +16,153 @@ namespace UserInterface.Views
{
public class AddCommandDialog : Window
{
private readonly IIpcClient<ServiceContractInterfaces> client;
public ComboBox comboBox { get; set; }
public ComboBox detectionModecomboBox { get; set; }
private readonly IIpcClient<IServiceContractInterfaces> _client;
public ComboBox ComboBox { get; set; }
public ComboBox DetectionModecomboBox { get; set; }
public Guid CommandId { get; }
public AddCommandDialog(Guid commandId) : this()
{
CommandId = commandId;
GetCommandInfo(CommandId);
Title = "Edit command";
}
public AddCommandDialog()
{
this.InitializeComponent();
InitializeComponent();
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;
ComboBox = this.FindControl<ComboBox>("ComboBox");
ComboBox.Items = Enum.GetValues(typeof(AvailableCommands)).Cast<AvailableCommands>().OrderBy(v => v.ToString());
ComboBox.SelectedIndex = 0;
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("addCommand", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("addCommand", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("addCommand");
_client = clientFactory.CreateClient("addCommand");
Title = "Add command";
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async void GetCommandInfo(Guid commandId)
{
var command = await _client.InvokeAsync(x => x.GetConfiguredCommand(commandId));
ComboBox.SelectedItem = command.Type;
FillDefaultValues();
ComboBox.IsEnabled = false;
var item = (AddCommandViewModel)DataContext;
item.SelectedType = command.Type;
item.Name = command.Name;
item.Command = command.Command;
item.Key = command.Key;
}
public async void Save(object sender, RoutedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
dynamic model = new { item.Name, item.Command, item.Key};
var item = (AddCommandViewModel)DataContext;
dynamic model = new { item.Name, item.Command, item.Key };
string json = JsonSerializer.Serialize(model);
await this.client.InvokeAsync(x => x.AddCommand(item.SelectedType, json));
if (CommandId == Guid.Empty)
await _client.InvokeAsync(x => x.AddCommand(item.SelectedType, json));
else
await _client.InvokeAsync(x => x.UpdateCommandById(CommandId, json));
Close();
}
public void ComboBoxClosed(object sender, SelectionChangedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
switch (this.comboBox.SelectedItem)
FillDefaultValues();
}
private void FillDefaultValues()
{
var item = (AddCommandViewModel)DataContext;
switch (ComboBox.SelectedItem)
{
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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#customcommand";
item.ShowCommandInput = true;
item.ShowKeyInput = false;
break;
case AvailableCommands.ShutdownCommand:
item.Description = "This command shuts down the PC immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#shutdowncommand";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#shutdowncommand";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.RestartCommand:
item.Description = "This command restarts the PC immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#restartcommand";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#restartcommand";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.HibernateCommand:
item.Description = "This command hibernates the PC immediately. ";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#hibernatecommand";
item.ShowCommandInput = false;
item.ShowKeyInput = 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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#logoffcommand";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.KeyCommand:
item.Description = "This command can be used to emulate a keystroke. It requires a key code which you can find by clicking the info button below.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#keycommand";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#keycommand";
item.ShowCommandInput = false;
item.ShowKeyInput = true;
break;
case AvailableCommands.PlayPauseCommand:
item.Description = "This command plays or pauses currently playing media.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.NextCommand:
item.Description = "This command skips to the next media.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.PreviousCommand:
item.Description = "This command plays previous media.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.VolumeDownCommand:
item.Description = "Lowers the system volume.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.VolumeUpCommand:
item.Description = "Raises the system volume.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
case AvailableCommands.MuteCommand:
item.Description = "Toggles muting the system volume.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#media-commands";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Commands.md#media-commands";
item.ShowCommandInput = false;
item.ShowKeyInput = false;
break;
@ -129,28 +174,26 @@ namespace UserInterface.Views
break;
}
}
public void OpenInfo(object sender, RoutedEventArgs args)
public void OpenInfo(object sender, RoutedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
var item = (AddCommandViewModel)DataContext;
BrowserUtil.OpenBrowser(item.MoreInfoLink);
}
public void Test(object sender, RoutedEventArgs args)
{
var item = ((AddCommandViewModel)this.DataContext);
var item = (AddCommandViewModel)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}";
var process = new System.Diagnostics.Process();
var startInfo = new System.Diagnostics.ProcessStartInfo
{
WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal,
FileName = "cmd.exe",
Arguments = $"/k {"echo You won't see this window normally. &&" + item.Command}"
};
process.StartInfo = startInfo;
process.Start();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
}

@ -5,27 +5,27 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="UserInterface.Views.AddSensorDialog"
SizeToContent="WidthAndHeight"
Title="Add sensor">
<StackPanel Margin="40" MinWidth="200">
<ContentControl Margin="0 20 0 10">Sensor type</ContentControl>
Title="Add / edit 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"/>
<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"/>
<ContentControl Margin="0 20 0 10">Update interval</ContentControl>
<StackPanel Orientation="Horizontal">
<Slider Value="{Binding UpdateInterval}" Minimum="1" Maximum="300" HorizontalAlignment="Left" Width="150"/>
<TextBox Text="{Binding UpdateInterval}" HorizontalAlignment="Right" MaxWidth="30"/>
</StackPanel>
<TextBlock Text="{Binding UpdateInterval, StringFormat= Update every {0} seconds}" HorizontalAlignment="Left" MinWidth="150"/>
<ContentControl IsVisible="{Binding ShowQueryInput}" Margin="0 20 0 10">Query</ContentControl>
<TextBox IsVisible="{Binding ShowQueryInput}" Text="{Binding Query}" Watermark="SELECT Name FROM Win32_Processor" HorizontalAlignment="Left" MinWidth="300"/>
<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"/>
<ContentControl Margin="0 20 0 10">Update interval</ContentControl>
<StackPanel Orientation="Horizontal">
<Slider Value="{Binding UpdateInterval}" Minimum="1" Maximum="300" HorizontalAlignment="Left" Width="150"/>
<TextBox Text="{Binding UpdateInterval}" HorizontalAlignment="Right" MaxWidth="30"/>
</StackPanel>
<TextBlock Text="{Binding UpdateInterval, StringFormat= Update every {0} seconds}" HorizontalAlignment="Left" MinWidth="150"/>
<ContentControl IsVisible="{Binding ShowQueryInput}" Margin="0 20 0 10">Query</ContentControl>
<TextBox IsVisible="{Binding ShowQueryInput}" Text="{Binding Query}" Watermark="SELECT Name FROM Win32_Processor" HorizontalAlignment="Left" MinWidth="300"/>
<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>
</StackPanel>
</StackPanel>
</Window>

@ -7,7 +7,6 @@ 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;
@ -17,163 +16,219 @@ namespace UserInterface.Views
{
public class AddSensorDialog : Window
{
private readonly IIpcClient<ServiceContractInterfaces> client;
public ComboBox comboBox { get; set; }
public ComboBox detectionModecomboBox { get; set; }
private readonly IIpcClient<IServiceContractInterfaces> _client;
public ComboBox ComboBox { get; set; }
public ComboBox DetectionModecomboBox { get; set; }
public Guid SensorId { get; }
public AddSensorDialog(Guid sensorId) : this()
{
SensorId = sensorId;
GetSensorInfo(SensorId);
Title = "Edit sensor";
}
public AddSensorDialog()
{
this.InitializeComponent();
InitializeComponent();
DataContext = new AddSensorViewModel();
this.comboBox = this.FindControl<ComboBox>("ComboBox");
this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast<AvailableSensors>().OrderBy(v => v.ToString());
this.comboBox.SelectedIndex = 0;
ComboBox = this.FindControl<ComboBox>("ComboBox");
ComboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast<AvailableSensors>().OrderBy(v => v.ToString());
ComboBox.SelectedIndex = 0;
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("addsensor", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("addsensor", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("addsensor");
_client = clientFactory.CreateClient("addsensor");
Title = "Add sensor";
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private async void GetSensorInfo(Guid sensorId)
{
ConfiguredSensorModel sensor = await _client.InvokeAsync(x => x.GetConfiguredSensor(sensorId));
ComboBox.SelectedItem = sensor.Type;
FillDefaultValues();
ComboBox.IsEnabled = false;
var item = (AddSensorViewModel)DataContext;
item.SelectedType = sensor.Type;
item.Name = sensor.Name;
item.UpdateInterval = sensor.UpdateInterval;
item.Query = sensor.Query;
item.WindowName = sensor.WindowName;
Title = $"Edit {sensor.Name}";
}
public async void Save(object sender, RoutedEventArgs args)
{
var item = ((AddSensorViewModel)this.DataContext);
dynamic model = new { item.Name, item.Query, item.UpdateInterval, item.WindowName};
var item = (AddSensorViewModel)DataContext;
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));
if (SensorId == Guid.Empty)
await _client.InvokeAsync(x => x.AddSensor(item.SelectedType, json));
else
await _client.InvokeAsync(x => x.UpdateSensorById(SensorId, json));
Close();
}
public void ComboBoxClosed(object sender, SelectionChangedEventArgs args)
{
var item = ((AddSensorViewModel)this.DataContext);
switch (this.comboBox.SelectedItem)
FillDefaultValues();
}
private void FillDefaultValues()
{
var item = (AddSensorViewModel)DataContext;
switch (ComboBox.SelectedItem)
{
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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#usernotificationstate";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.DummySensor:
item.Description = "This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#dummy";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#dummysensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 1;
break;
case AvailableSensors.CPULoadSensor:
item.Description = "This sensor checks the current CPU load. It averages the load on all logical cores every second and rounds the output to two decimals.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#cpuload";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#cpuloadsensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.CurrentClockSpeedSensor:
item.Description = "This sensor returns the BIOS configured baseclock for the processor.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#currentclockspeed";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#currentclockspeedsensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 3600;
break;
case AvailableSensors.WMIQuerySensor:
item.Description = "This advanced sensor executes a user defined WMI query and exposes the result. The query should return a single value.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#wmiquerysensor";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipperhass-workstation-service/blob/master/documentation/WMIQuery.md#wmiquerysensor";
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#usedmemory";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#memoryusagesensorsensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 10;
break;
case AvailableSensors.ActiveWindowSensor:
item.Description = "This sensor exposes the name of the currently active window.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#activewindow";
item.ShowDetectionModeOptions = false;
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#activewindowsensor";
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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#webcamactivesensor";
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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#microphoneactivesensor";
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";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#namedwindowsensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = true;
item.UpdateInterval = 5;
break;
case AvailableSensors.LastActiveSensor:
item.Description = "This sensor returns the date/time that the workstation was last active.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#lastactive";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#lastactivesensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.LastBootSensor:
item.Description = "This sensor returns the date/time that Windows was last booted";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#lastboot";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#lastbootsensor";
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.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#sessionstatesensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.CurrentVolumeSensor:
item.Description = "This sensor returns the volume of currently playing audio.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#currentvolume";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#currentvolumesensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.MasterVolumeSensor:
item.Description = "This sensor returns the master volume of the currently selected default audio device as a percentage value.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#mastervolumesensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.GPUTemperatureSensor:
item.Description = "This sensor returns the current temperature of the GPU in °C.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#gputemperature";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#gputemperaturesensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
case AvailableSensors.GPULoadSensor:
item.Description = "This sensor returns the current GPU load.";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#gpuload";
item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/Sensors.md#gpuloadsensor";
item.ShowQueryInput = false;
item.ShowWindowNameInput = false;
item.UpdateInterval = 5;
break;
default:
item.Description = null;
item.MoreInfoLink = null;
@ -181,15 +236,11 @@ namespace UserInterface.Views
break;
}
}
public void OpenInfo(object sender, RoutedEventArgs args)
{
var item = ((AddSensorViewModel)this.DataContext);
var item = (AddSensorViewModel)DataContext;
BrowserUtil.OpenBrowser(item.MoreInfoLink);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -13,6 +13,14 @@
<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>
<TextBlock Margin="0 0 0 20">
Having issues? Check out the log files.
Using the configuration files is recommended
for advanced users only.</TextBlock >
<StackPanel Margin="0 0 0 20" HorizontalAlignment="Left" Orientation="Horizontal">
<Button Width="75" HorizontalAlignment="Right" Margin="10 10" Click="OpenLogDirectory">Log files</Button>
<Button Width="75" HorizontalAlignment="Left" Margin="10 10" Click="OpenConfigDirectory">Config files</Button>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

@ -11,28 +11,31 @@ using UserInterface.ViewModels;
using System.Security;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using UserInterface.Util;
using System;
using System.IO;
using System.Diagnostics;
namespace UserInterface.Views
{
public class AppInfo : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private readonly IIpcClient<IServiceContractInterfaces> client;
private readonly string _basePath;
public AppInfo()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("info", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("info", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("info");
this._basePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service");
@ -63,6 +66,17 @@ namespace UserInterface.Views
BrowserUtil.OpenBrowser("https://discord.gg/VraYT2N3wd");
}
public void OpenLogDirectory(object sender, RoutedEventArgs args)
{
string path = Path.Combine(this._basePath, "logs");
Process.Start("explorer.exe", path);
}
public void OpenConfigDirectory(object sender, RoutedEventArgs args)
{
Process.Start("explorer.exe", this._basePath);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);

@ -15,49 +15,50 @@ namespace UserInterface.Views
{
public class BackgroundServiceSettings : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private readonly IIpcClient<IServiceContractInterfaces> _client;
public BackgroundServiceSettings()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("broker", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("broker", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("broker");
this._client = clientFactory.CreateClient("broker");
DataContext = new BackgroundServiceSettingsViewModel();
Ping();
}
public async void Ping() {
public async void Ping()
{
while (true)
{
if (DataContext is not BackgroundServiceSettingsViewModel viewModel)
throw new System.Exception("Wrong viewmodel class!");
try
{
var result = await this.client.InvokeAsync(x => x.Ping("ping"));
var result = await this._client.InvokeAsync(x => x.Ping("ping"));
if (result == "pong")
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(true, "All good");
}
viewModel.UpdateStatus(true, "All good");
else
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(false, "Not running");
}
viewModel.UpdateStatus(false, "Not running");
}
catch (System.Exception)
{
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateStatus(false, "Not running");
viewModel.UpdateStatus(false, "Not running");
}
var autostartresult = await this.client.InvokeAsync(x => x.IsAutoStartEnabled());
((BackgroundServiceSettingsViewModel)this.DataContext).UpdateAutostartStatus(autostartresult);
var autostartresult = await this._client.InvokeAsync(x => x.IsAutoStartEnabled());
viewModel.UpdateAutostartStatus(autostartresult);
await Task.Delay(1000);
}
@ -71,11 +72,12 @@ namespace UserInterface.Views
public void EnableAutostart(object sender, RoutedEventArgs args)
{
this.client.InvokeAsync(x => x.EnableAutostart(true));
this._client.InvokeAsync(x => x.EnableAutostart(true));
}
public void DisableAutostart(object sender, RoutedEventArgs args)
{
this.client.InvokeAsync(x => x.EnableAutostart(false));
this._client.InvokeAsync(x => x.EnableAutostart(false));
}
private void InitializeComponent()
@ -83,4 +85,4 @@ namespace UserInterface.Views
AvaloniaXamlLoader.Load(this);
}
}
}
}

@ -17,19 +17,19 @@ namespace UserInterface.Views
{
public class BrokerSettings : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private readonly IIpcClient<IServiceContractInterfaces> client;
public BrokerSettings()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("broker", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("broker", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("broker");

@ -3,24 +3,27 @@
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}">
x:Class="UserInterface.Views.CommandSettings">
<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>
<Grid RowDefinitions="Auto, *, Auto, Auto" Margin="30 30 30 10" >
<TextBlock Grid.Row="0" Margin="0 0 0 15" FontSize="18" FontWeight="Bold" Text="Commands"/>
<DataGrid Grid.Row="1" 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 Grid.Row="2" IsVisible="{Binding !ConfiguredCommands.Count}" Text="Add some commands by clicking the 'Add' button."/>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="75" Margin="10 10 0 0" Click="AddCommand" Content="Add"/>
<Button Width="75" Margin="10 10 0 0" Click="EditCommand" Content="Edit"/>
<Button Width="75" Margin="10 10 0 0" Click="DeleteCommand" Content="Delete"/>
</StackPanel>
</Grid>
</UserControl>

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

@ -0,0 +1,27 @@
<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.GeneralSettingsView">
<StackPanel Margin="30" HorizontalAlignment="Left">
<ContentControl FontSize="18" FontWeight="Bold">Settings</ContentControl>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Name prefix</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
This allows you to set a name which will be used to prefix all sensor- and command names. For example:
If a sensor is called "ActiveWindow" and the name prefix is set to "laptop", the sensor will be named "laptop-ActiveWindow" and its entityId will be "laptop_activewindow".
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding NamePrefix}" HorizontalAlignment="Left" Width="100"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
</StackPanel>
</UserControl>

@ -0,0 +1,66 @@
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.ComponentModel.DataAnnotations;
using System.Collections.Generic;
using hass_workstation_service.Data;
namespace UserInterface.Views
{
public class GeneralSettingsView : UserControl
{
private readonly IIpcClient<IServiceContractInterfaces> client;
public GeneralSettingsView()
{
this.InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<IServiceContractInterfaces>("general", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("general");
DataContext = new GeneralSettingsViewModel();
GetSettings();
}
public void Configure(object sender, RoutedEventArgs args)
{
var model = (GeneralSettingsViewModel)this.DataContext;
ICollection<ValidationResult> results;
if (model.IsValid(model, out results))
{
var result = this.client.InvokeAsync(x => x.WriteGeneralSettings(new GeneralSettings() { NamePrefix = model.NamePrefix }));
}
}
public async void GetSettings()
{
GeneralSettings settings = await this.client.InvokeAsync(x => x.GetGeneralSettings());
((GeneralSettingsViewModel)this.DataContext).Update(settings);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

@ -4,23 +4,24 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:UserInterface.Views"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="500"
x:Class="UserInterface.Views.MainWindow"
Icon="/Assets/hass-workstation-logo.ico"
SizeToContent="WidthAndHeight"
MinWidth="700"
MinHeight="500"
Title="Settings">
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<ScrollViewer Height="700" VerticalScrollBarVisibility="Visible">
<Grid ColumnDefinitions="Auto,1*,Auto" RowDefinitions="Auto,Auto,Auto" Margin="30" VerticalAlignment="Stretch">
<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: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:AppInfo Grid.Column="2" Grid.Row="1" Margin="10 10 10 0" Background="#2D2D30"/>
</Grid>
</ScrollViewer>
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
</Window>
<Grid ColumnDefinitions="Auto, *, Auto" RowDefinitions="*, *" Margin="10">
<views:BrokerSettings Grid.Column="0" Grid.Row="0" Margin="10" Grid.RowSpan="2" Background="#2D2D30"/>
<views:GeneralSettingsView Grid.Column="0" Grid.Row="1" Margin="10" Grid.RowSpan="2" Background="#2D2D30"/>
<views:SensorSettings Grid.Column="1" Grid.Row="0" Margin="10" Background="#2D2D30"/>
<views:CommandSettings Grid.Column="1" Grid.Row="1" Margin="10" Background="#2D2D30"/>
<views:BackgroundServiceSettings Grid.Column="2" Grid.Row="0" Margin="10" Background="#2D2D30"/>
<views:AppInfo Grid.Column="2" Grid.Row="1" Margin="10" Background="#2D2D30"/>
</Grid>
</Window>

@ -11,7 +11,7 @@ namespace UserInterface.Views
{
InitializeComponent();
WindowsTrayIcon icon = new WindowsTrayIcon();
_ = new WindowsTrayIcon();
}
private void InitializeComponent()
@ -19,4 +19,4 @@ namespace UserInterface.Views
AvaloniaXamlLoader.Load(this);
}
}
}
}

@ -3,30 +3,33 @@
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.SensorSettings" >
<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}">
x:Class="UserInterface.Views.SensorSettings">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"
Width="1*" />
<DataGridTextColumn Header="Type"
Binding="{Binding Type}"
Width="1*" />
<DataGridTextColumn Header="Update Interval"
Binding="{Binding UpdateInterval}"
Width="1*" />
<DataGridTextColumn Header="Value"
Binding="{Binding ValueString}"
Width="2*" />
</DataGrid.Columns>
</DataGrid>
<TextBlock IsVisible="{Binding !ConfiguredSensors.Count}">Add some sensors by clicking the "Add" button. </TextBlock>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="AddSensor">Add</Button>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Delete">Delete</Button>
</StackPanel>
</UserControl>
<Grid RowDefinitions="Auto, *, Auto, Auto" Margin="30 30 30 10" >
<TextBlock Grid.Row="0" Margin="0 0 0 15" FontSize="18" FontWeight="Bold" Text="Sensors"/>
<DataGrid Grid.Row="1" x:Name="Grid" IsVisible="{Binding ConfiguredSensors.Count}"
AutoGenerateColumns="False" IsReadOnly="True" SelectionMode="Single"
Items="{Binding ConfiguredSensors}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"
Width="1*" />
<DataGridTextColumn Header="Type"
Binding="{Binding Type}"
Width="1*" />
<DataGridTextColumn Header="Update Interval"
Binding="{Binding UpdateInterval}"
Width="1*" />
<DataGridTextColumn Header="Value"
Binding="{Binding ValueString}"
Width="2*" />
</DataGrid.Columns>
</DataGrid>
<TextBlock Grid.Row="2" IsVisible="{Binding !ConfiguredSensors.Count}" Text="Add some sensors by clicking the 'Add' button."/>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Width="75" Margin="10 10 0 0" Click="AddSensor" Content="Add"/>
<Button Width="75" Margin="10 10 0 0" Click="EditSensor" Content="Edit"/>
<Button Width="75" Margin="10 10 0 0" Click="DeleteSensor" Content="Delete"/>
</StackPanel>
</Grid>
</UserControl>

@ -8,7 +8,6 @@ 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;
@ -18,48 +17,61 @@ namespace UserInterface.Views
{
public class SensorSettings : UserControl
{
private readonly IIpcClient<ServiceContractInterfaces> client;
private DataGrid _dataGrid { get; set; }
private bool sensorsNeedToRefresh { get; set; }
private readonly IIpcClient<IServiceContractInterfaces> _client;
private readonly DataGrid _dataGrid;
private bool _sensorsNeedToRefresh;
public SensorSettings()
{
this.InitializeComponent();
InitializeComponent();
// register IPC clients
ServiceProvider serviceProvider = new ServiceCollection()
.AddNamedPipeIpcClient<ServiceContractInterfaces>("sensors", pipeName: "pipeinternal")
.AddNamedPipeIpcClient<IServiceContractInterfaces>("sensors", pipeName: "pipeinternal")
.BuildServiceProvider();
// resolve IPC client factory
IIpcClientFactory<ServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<ServiceContractInterfaces>>();
IIpcClientFactory<IServiceContractInterfaces> clientFactory = serviceProvider
.GetRequiredService<IIpcClientFactory<IServiceContractInterfaces>>();
// create client
this.client = clientFactory.CreateClient("sensors");
_client = clientFactory.CreateClient("sensors");
_dataGrid = this.FindControl<DataGrid>("Grid");
DataContext = new SensorSettingsViewModel();
GetConfiguredSensors();
this._dataGrid = this.FindControl<DataGrid>("Grid");
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public async void GetConfiguredSensors()
{
sensorsNeedToRefresh = false;
List<ConfiguredSensorModel> status = await this.client.InvokeAsync(x => x.GetConfiguredSensors());
_sensorsNeedToRefresh = false;
List<ConfiguredSensorModel> status = await _client.InvokeAsync(x => x.GetConfiguredSensors());
((SensorSettingsViewModel)this.DataContext).ConfiguredSensors = status.Select(s => new SensorViewModel() { Name = s.Name, Type = s.Type, Value = s.Value, Id = s.Id, UpdateInterval = s.UpdateInterval, UnitOfMeasurement = s.UnitOfMeasurement }).ToList();
while (!sensorsNeedToRefresh)
((SensorSettingsViewModel)DataContext).ConfiguredSensors = status.Select(s =>
new SensorViewModel()
{
Name = s.Name,
Type = s.Type,
Value = s.Value,
Id = s.Id,
UpdateInterval = s.UpdateInterval,
UnitOfMeasurement = s.UnitOfMeasurement
}).ToList();
while (!_sensorsNeedToRefresh)
{
await Task.Delay(1000);
List<ConfiguredSensorModel> statusUpdated = await this.client.InvokeAsync(x => x.GetConfiguredSensors());
var configuredSensors = ((SensorSettingsViewModel)this.DataContext).ConfiguredSensors;
List<ConfiguredSensorModel> statusUpdated = await _client.InvokeAsync(x => x.GetConfiguredSensors());
var configuredSensors = ((SensorSettingsViewModel)DataContext).ConfiguredSensors;
// this is a workaround for the list showing before it has been completely loaded in the service
if (statusUpdated.Count != configuredSensors.Count) {
sensorsNeedToRefresh = true;
GetConfiguredSensors();
if (statusUpdated.Count != configuredSensors.Count)
{
_sensorsNeedToRefresh = true;
GetConfiguredSensors();
}
statusUpdated.ForEach(s =>
{
@ -72,33 +84,46 @@ namespace UserInterface.Views
}
});
}
}
public void Delete(object sender, RoutedEventArgs args)
{
var item = ((SensorViewModel)this._dataGrid.SelectedItem);
this.client.InvokeAsync(x => x.RemoveSensorById(item.Id));
((SensorSettingsViewModel)this.DataContext).ConfiguredSensors.Remove(item);
this._dataGrid.SelectedIndex = -1;
((SensorSettingsViewModel)this.DataContext).TriggerUpdate();
}
public async void AddSensor(object sender, RoutedEventArgs args)
{
AddSensorDialog dialog = new AddSensorDialog();
var dialog = new AddSensorDialog();
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
await dialog.ShowDialog(desktop.MainWindow);
sensorsNeedToRefresh = true;
_sensorsNeedToRefresh = true;
GetConfiguredSensors();
}
}
private void InitializeComponent()
public async void EditSensor(object sender, RoutedEventArgs args)
{
AvaloniaXamlLoader.Load(this);
if (_dataGrid.SelectedItem is not SensorViewModel item)
return;
var dialog = new AddSensorDialog(item.Id);
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
await dialog.ShowDialog(desktop.MainWindow);
_sensorsNeedToRefresh = true;
GetConfiguredSensors();
}
}
public void DeleteSensor(object sender, RoutedEventArgs args)
{
if (_dataGrid.SelectedItem is not SensorViewModel item)
return;
_client.InvokeAsync(x => x.RemoveSensorById(item.Id));
if (DataContext is not SensorSettingsViewModel viewModel)
return;
viewModel.ConfiguredSensors.Remove(item);
_dataGrid.SelectedIndex = -1;
viewModel.TriggerUpdate();
}
}
}
}

@ -0,0 +1,47 @@
# 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 the 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`.
### HibernateCommand
This command hibernates the computer immediately. It runs `shutdown /h`.
### 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|
|---|---|
|shutdown /s /f /t 000|Forcefully shutdown the PC immediately.|
|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.|
### KeyCommand
Sends a keystroke with the specified key. You can pick [any of these](https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes) key codes.
### Media Commands
There's several media commands available that allow you to control media playback. You can combine them into a media player entity as shown [here](https://pastebin.com/1VdL5iQm).
|Command|use|
|---|---|
|Play/Pause|The same as pressing the play/pause media key|
|Next|skip to next track|
|Previous|skip to previous track|
|Volume up|Increase system master volume|
|Volume down|Decrease system master volume|
|Mute (toggle)|Mute the system|

@ -0,0 +1,90 @@
# Sensors
Sensors are used to transfer data about the host system to an automation hub, where it can be processed. `hass-workstation-service` provides many sensors, and they are lisited below in the same order as listed in the configuration GUI.
### ActiveWindowSensor
The active window sensor returns the title of the currently selected window, and is the same value shown when hovering over the icon in the windows task bar.
This sensor is commonly used to trigger automations when a specific program is in use, such as pausing audio during a skype call. This sensor can be unreliable when used with applications such as web browsers that update their title based on the current context. You can partially resolve this using regular expressions.
### CPULoadSensor
The CPU load sensor is used to determine the current utilization of the CPU, and returns a percentage value.
### CurrentClockSpeedSensor
The current clock speed sensor returns the base system clock as configured in the bios. **It does not return the current operating frequency of the CPU**
### CurrentVolumeSensor
This sensor returns the current volume of playing audio. **It does not return the master volume.** If you have no sound playing the value will be 0.
### MasterVolumeSensor
This sensor returns the master volume for the currently selected default audio device.
### DummySensor
This sensor produces a random output every second, and is intended to test latency and connectivity.
### GPULoadSensor
This is the same as the [CPULoadSensor](https://github.com/sleevezipper/hass-workstation-service/new/master/documentation#cpuloadsensor), but for the GPU.
### GPUTemperatureSensor
The GPU temperature returns the current operating temperature of the GPU. This sensor is useful for controling external cooling systems such as air conditioning.
### LastActiveSensor
The last active sensor returns the time when the workstation was last active (last keyboard and mouse input). It is useful as a form of presence detection when combined with motion sensors or software such as room assistant, although may not be reliable if used with auto clickers or other macro software commonly used for video game automation.
### LastBootSensor
The last boot sensor returns the time the windows computer booted. It can be used to calculate uptime, if combined with another sensor to detect system shutdowns.
### MemoryUsageSensor
This returns the amount of system memory used as a percentage value, as indicated by the task manager.
### MicrophoneActiveSensor
This is a binary sensor that can be used to detect if the microphone is in use. **It does not return what process is using it**
### NamedWindowSensor
The named window sensor is similar to the [ActiveWindowSensor](https://github.com/sleevezipper/hass-workstation-service/new/master/documentation#activewindowsensor), however it is a binary sensor that returns true if a window with a title matching a pre determined value is detected.
### SessionStateSensor
The session state sensor can be used to detect if someone is logged in. It has the following values :
|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.|
### UserNotificationState
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. Notice that this status does not watch Focus Assist. It has the following possible states:
|State|Explanation|
|---|---|
|NotPresent|A screen saver is displayed, the machine is locked, or a nonactive Fast User Switching session is in progress. |
|Busy|A full-screen application is running or Presentation Settings are applied. Presentation Settings allow a user to put their machine into a state fit for an uninterrupted presentation, such as a set of PowerPoint slides, with a single click.|
|RunningDirect3dFullScreen|A full-screen (exclusive mode) Direct3D application is running.|
|PresentationMode|The user has activated Windows presentation settings to block notifications and pop-up messages.|
|AcceptsNotifications|None of the other states are found, notifications can be freely sent.|
|QuietTime|Introduced in Windows 7. The current user is in "quiet time", which is the first hour after a new user logs into his or her account for the first time. During this time, most notifications should not be sent or shown. This lets a user become accustomed to a new computer system without those distractions. Quiet time also occurs for each user after an operating system upgrade or clean installation.|
|RunningWindowsStoreApp|A Windows Store app is running.|
### WebcamActiveSensor
The webcam active sensor returns the status of the webcam.
### WMIQuerySensor
Please see the specific documentaion page [here](https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/WMIQuery.md#wmiquerysensor).

@ -0,0 +1,21 @@
# WMIQuerySensor
The WMI query sensor is an advanced sensor that executes a user defined [WMI query](https://docs.microsoft.com/en-us/windows/win32/wmisdk/wmi-and-sql) and exposes the result.
To use the WMI query sensor, you should create a WMI query and paste it in the box. For example, If you wanted to find the current CPU frequency you can use this command:
```sql
SELECT CurrentClockSpeed FROM Win32_Processor
```
which results in `4008` for my PC. Because this query retuens a single value (CPU frequency in MHz), it can be used with the current WMI query sensor implementation.
The command ```sql SELECT * FROM Win32_Processor``` cannot be used because it returns `|64|9|To Be Filled By O.E.M.|3| ... |GAME-PC-2016|8|1|False|False|`, and the current WMI query implementation only supports commands that return a single value.
You can use [WMI Explorer](https://github.com/vinaypamnani/wmie2/tree/v2.0.0.2) to construct a query, or alternatively look at the user submited sensors below:
|Query|Explanation|Submitted by|
|---|---|---|
|`SELECT username FROM Win32_ComputerSystem`|Shows the current user|@grizzlyjere|
|`Select * from Win32_Process Where Name = 'notepad.exe'`|Shows if the defined process is running|@lafferlaffer|

@ -23,7 +23,7 @@ We can execute the shutdown command, and turn off the media center PC.
## Reading sensor data
Reading sensor data can be done by subscribing to an MQTT topic. For example, if I wanted to know the CPU load, i can subscribe to this topic :
Reading sensor data can be done by subscribing to an MQTT topic. For example, if I wanted to know the CPU load, I can subscribe to this topic :
```
homeassistant/sensor/DESKTOP-1/MediaCenterPC/State
```

@ -1,25 +1,31 @@
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Data;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace hass_workstation_service.Communication.NamedPipe
{
public interface ServiceContractInterfaces
public interface IServiceContractInterfaces
{
Task<MqttSettings> GetMqttBrokerSettings();
Task<GeneralSettings> GetGeneralSettings();
void WriteGeneralSettings(GeneralSettings settings);
public string Ping(string str);
void WriteMqttBrokerSettingsAsync(MqttSettings settings);
MqqtClientStatus GetMqqtClientStatus();
void EnableAutostart(bool enable);
bool IsAutoStartEnabled();
Task<ConfiguredSensorModel> GetConfiguredSensor(Guid id);
Task<List<ConfiguredSensorModel>> GetConfiguredSensors();
void RemoveSensorById(Guid id);
void AddSensor(AvailableSensors sensorType, string json);
void RemoveCommandById(Guid id);
void RemoveSensorById(Guid id);
void UpdateSensorById(Guid id, string json);
ConfiguredCommandModel GetConfiguredCommand(Guid id);
List<ConfiguredCommandModel> GetConfiguredCommands();
void AddCommand(AvailableCommands commandType, string json);
void RemoveCommandById(Guid id);
void UpdateCommandById(Guid id, string json);
string GetCurrentVersion();
}
}
}

@ -7,15 +7,13 @@ using hass_workstation_service.Domain.Sensors;
using Serilog;
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace hass_workstation_service.Communication.InterProcesCommunication
{
public class InterProcessApi : ServiceContractInterfaces
public class InterProcessApi : IServiceContractInterfaces
{
private readonly MqttPublisher _publisher;
private readonly IConfigurationService _configurationService;
@ -26,73 +24,89 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
_configurationService = configurationService;
}
public MqqtClientStatus GetMqqtClientStatus()
{
return this._publisher.GetStatus();
}
public MqqtClientStatus GetMqqtClientStatus() => _publisher.GetStatus();
public Task<MqttSettings> GetMqttBrokerSettings()
{
return this._configurationService.GetMqttBrokerSettings();
}
public Task<MqttSettings> GetMqttBrokerSettings() => _configurationService.GetMqttBrokerSettings();
/// <summary>
/// You can use this to check if the application responds.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
public string Ping(string str)
{
if (str == "ping")
{
return "pong";
}
return "what?";
}
public string Ping(string str) => str == "ping" ? "pong" : "what?";
public string GetCurrentVersion() => Program.GetVersion();
/// <summary>
/// This writes the provided settings to the config file.
/// </summary>
/// <param name="settings"></param>
public void WriteMqttBrokerSettingsAsync(MqttSettings settings)
{
this._configurationService.WriteMqttBrokerSettingsAsync(settings);
}
public void WriteMqttBrokerSettingsAsync(MqttSettings settings) => _configurationService.WriteMqttBrokerSettingsAsync(settings);
/// <summary>
/// Enables or disables autostart.
/// </summary>
/// <param name="enable"></param>
public void EnableAutostart(bool enable)
{
this._configurationService.EnableAutoStart(enable);
}
public void EnableAutostart(bool enable) => _configurationService.EnableAutoStart(enable);
public bool IsAutoStartEnabled()
{
return this._configurationService.IsAutoStartEnabled();
}
public bool IsAutoStartEnabled() => _configurationService.IsAutoStartEnabled();
public async Task<List<ConfiguredSensorModel>> GetConfiguredSensors()
{
var sensors = await this._configurationService.GetSensorsAfterLoadingAsync();
return sensors.Select(s => new ConfiguredSensorModel() { Name = s.Name, Type = s.GetType().Name, Value = s.PreviousPublishedState, Id = s.Id, UpdateInterval = s.UpdateInterval, UnitOfMeasurement = ((SensorDiscoveryConfigModel)s.GetAutoDiscoveryConfig()).Unit_of_measurement }).ToList();
var sensors = await _configurationService.GetSensorsAfterLoadingAsync();
return sensors.Select(s =>
{
if (!Enum.TryParse(s.GetType().Name, out AvailableSensors type))
Log.Logger.Error("Unknown sensor");
return new ConfiguredSensorModel(s);
}).ToList();
}
public List<ConfiguredCommandModel> GetConfiguredCommands()
public async Task<ConfiguredSensorModel> GetConfiguredSensor(Guid id)
{
return this._configurationService.ConfiguredCommands.Select(s => new ConfiguredCommandModel() { Name = s.Name, Type = s.GetType().Name, Id = s.Id }).ToList();
var sensors = await _configurationService.GetSensorsAfterLoadingAsync();
var s = sensors.FirstOrDefault(x => id == x.Id);
if (s == null)
return null;
else
{
if (!Enum.TryParse(s.GetType().Name, out AvailableSensors type))
Log.Logger.Error("Unknown sensor");
return new ConfiguredSensorModel(s);
}
}
public void RemoveCommandById(Guid id)
public List<ConfiguredCommandModel> GetConfiguredCommands()
{
this._configurationService.DeleteConfiguredCommand(id);
return _configurationService.ConfiguredCommands.Select(c =>
{
if (!Enum.TryParse(c.GetType().Name, out AvailableCommands type))
Log.Logger.Error("Unknown command");
return new ConfiguredCommandModel(c);
}).ToList();
}
public void RemoveSensorById(Guid id)
public ConfiguredCommandModel GetConfiguredCommand(Guid id)
{
this._configurationService.DeleteConfiguredSensor(id);
var c = _configurationService.ConfiguredCommands.FirstOrDefault(x => id == x.Id);
if (c == null)
return null;
else
{
if (!Enum.TryParse(c.GetType().Name, out AvailableCommands type))
Log.Logger.Error("Unknown command");
return new ConfiguredCommandModel(c);
}
}
public void RemoveSensorById(Guid id) => _configurationService.DeleteConfiguredSensor(id);
public void RemoveCommandById(Guid id) => _configurationService.DeleteConfiguredCommand(id);
/// <summary>
/// Adds a command to the configured commands. This properly initializes the class and writes it to the config file.
/// </summary>
@ -100,71 +114,12 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
/// <param name="json"></param>
public void AddSensor(AvailableSensors sensorType, string json)
{
var serializerOptions = new JsonSerializerOptions
{
Converters = { new DynamicJsonConverter() }
};
dynamic model = JsonSerializer.Deserialize<dynamic>(json, serializerOptions);
var sensorToCreate = GetSensorToCreate(sensorType, json);
AbstractSensor sensorToCreate = null;
switch (sensorType)
{
case AvailableSensors.UserNotificationStateSensor:
sensorToCreate = new UserNotificationStateSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.DummySensor:
sensorToCreate = new DummySensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.CurrentClockSpeedSensor:
sensorToCreate = new CurrentClockSpeedSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.CPULoadSensor:
sensorToCreate = new CPULoadSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.WMIQuerySensor:
sensorToCreate = new WMIQuerySensor(this._publisher, model.Query, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.MemoryUsageSensor:
sensorToCreate = new MemoryUsageSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
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.LastActiveSensor:
sensorToCreate = new LastActiveSensor(this._publisher,(int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.LastBootSensor:
sensorToCreate = new LastBootSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.SessionStateSensor:
sensorToCreate = new SessionStateSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.CurrentVolumeSensor:
sensorToCreate = new CurrentVolumeSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.GPUTemperatureSensor:
sensorToCreate = new GpuTemperatureSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
case AvailableSensors.GPULoadSensor:
sensorToCreate = new GpuLoadSensor(this._publisher, (int)model.UpdateInterval, model.Name);
break;
default:
Log.Logger.Error("Unknown sensortype");
break;
}
if (sensorToCreate != null)
{
this._configurationService.AddConfiguredSensor(sensorToCreate);
}
if (sensorToCreate == null)
Log.Logger.Error("Unknown sensortype");
else
_configurationService.AddConfiguredSensor(sensorToCreate);
}
/// <summary>
@ -173,6 +128,38 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
/// <param name="commandType"></param>
/// <param name="json"></param>
public void AddCommand(AvailableCommands commandType, string json)
{
var commandToCreate = GetCommandToCreate(commandType, json);
if (commandToCreate == null)
Log.Logger.Error("Unknown command type");
else
_configurationService.AddConfiguredCommand(commandToCreate);
}
public async void UpdateSensorById(Guid id, string json)
{
var existingSensor = await GetConfiguredSensor(id);
var sensorToUpdate = GetSensorToCreate(existingSensor.Type, json);
if (sensorToUpdate == null)
Log.Logger.Error("Unknown sensortype");
else
_configurationService.UpdateConfiguredSensor(id, sensorToUpdate);
}
public void UpdateCommandById(Guid id, string json)
{
var existingCommand = GetConfiguredCommand(id);
var commandToUpdate = GetCommandToCreate(existingCommand.Type, json);
if (commandToUpdate == null)
Log.Logger.Error("Unknown commandtype");
else
_configurationService.UpdateConfiguredCommand(id, commandToUpdate);
}
private AbstractSensor GetSensorToCreate(AvailableSensors sensorType, string json)
{
var serializerOptions = new JsonSerializerOptions
{
@ -180,55 +167,58 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
};
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;
case AvailableCommands.PlayPauseCommand:
commandToCreate = new MediaPlayPauseCommand(this._publisher, model.Name);
break;
case AvailableCommands.NextCommand:
commandToCreate = new MediaNextCommand(this._publisher, model.Name);
break;
case AvailableCommands.PreviousCommand:
commandToCreate = new MediaPreviousCommand(this._publisher, model.Name);
break;
case AvailableCommands.VolumeUpCommand:
commandToCreate = new MediaVolumeUpCommand(this._publisher, model.Name);
break;
case AvailableCommands.VolumeDownCommand:
commandToCreate = new MediaVolumeDownCommand(this._publisher, model.Name);
break;
case AvailableCommands.MuteCommand:
commandToCreate = new MediaMuteCommand(this._publisher, model.Name);
break;
case AvailableCommands.KeyCommand:
commandToCreate = new KeyCommand(this._publisher, Convert.ToByte(model.Key, 16), model.Name);
break;
default:
Log.Logger.Error("Unknown sensortype");
break;
}
if (commandToCreate != null)
return sensorType switch
{
this._configurationService.AddConfiguredCommand(commandToCreate);
}
AvailableSensors.UserNotificationStateSensor => new UserNotificationStateSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.DummySensor => new DummySensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.CurrentClockSpeedSensor => new CurrentClockSpeedSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.CPULoadSensor => new CPULoadSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.WMIQuerySensor => new WMIQuerySensor(_publisher, model.Query, (int)model.UpdateInterval, model.Name),
AvailableSensors.MemoryUsageSensor => new MemoryUsageSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.ActiveWindowSensor => new ActiveWindowSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.WebcamActiveSensor => new WebcamActiveSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.MicrophoneActiveSensor => new MicrophoneActiveSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.NamedWindowSensor => new NamedWindowSensor(_publisher, model.WindowName, model.Name, (int)model.UpdateInterval),
AvailableSensors.LastActiveSensor => new LastActiveSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.LastBootSensor => new LastBootSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.SessionStateSensor => new SessionStateSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.CurrentVolumeSensor => new CurrentVolumeSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.GPUTemperatureSensor => new GpuTemperatureSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.GPULoadSensor => new GpuLoadSensor(_publisher, (int)model.UpdateInterval, model.Name),
AvailableSensors.MasterVolumeSensor => new MasterVolumeSensor(_publisher, (int)model.UpdateInterval, model.Name),
_ => null
};
}
public string GetCurrentVersion()
private AbstractCommand GetCommandToCreate(AvailableCommands commandType, string json)
{
return Program.GetVersion();
var serializerOptions = new JsonSerializerOptions
{
Converters = { new DynamicJsonConverter() }
};
dynamic model = JsonSerializer.Deserialize<dynamic>(json, serializerOptions);
return commandType switch
{
AvailableCommands.ShutdownCommand => new ShutdownCommand(_publisher, model.Name),
AvailableCommands.RestartCommand => new RestartCommand(_publisher, model.Name),
AvailableCommands.HibernateCommand => new HibernateCommand(_publisher, model.Name),
AvailableCommands.LogOffCommand => new LogOffCommand(_publisher, model.Name),
AvailableCommands.CustomCommand => new CustomCommand(_publisher, model.Command, model.Name),
AvailableCommands.PlayPauseCommand => new PlayPauseCommand(_publisher, model.Name),
AvailableCommands.NextCommand => new NextCommand(_publisher, model.Name),
AvailableCommands.PreviousCommand => new PreviousCommand(_publisher, model.Name),
AvailableCommands.VolumeUpCommand => new VolumeUpCommand(_publisher, model.Name),
AvailableCommands.VolumeDownCommand => new VolumeDownCommand(_publisher, model.Name),
AvailableCommands.MuteCommand => new MuteCommand(_publisher, model.Name),
AvailableCommands.KeyCommand => new KeyCommand(_publisher, Convert.ToByte(model.Key, 16), model.Name),
_ => null
};
}
public Task<GeneralSettings> GetGeneralSettings() => _configurationService.ReadGeneralSettings();
public void WriteGeneralSettings(GeneralSettings settings) => _configurationService.WriteGeneralSettingsAsync(settings);
}
}
}

@ -1,7 +1,6 @@
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors;
using System;
using System.Collections.Generic;
using System.Text;
namespace hass_workstation_service.Communication.InterProcesCommunication.Models
{
@ -23,19 +22,68 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
public class ConfiguredSensorModel
{
public Guid Id { get; set; }
public string Type { get; set; }
public AvailableSensors Type { get; set; }
public string Name { get; set; }
public string Value { get; set; }
public string Query { get; set; }
public string WindowName { get; set; }
public int UpdateInterval { get; set; }
public string UnitOfMeasurement { get; set; }
public ConfiguredSensorModel(AbstractSensor sensor)
{
this.Id = sensor.Id;
this.Name = sensor.Name;
Enum.TryParse(sensor.GetType().Name, out AvailableSensors type);
this.Type = type;
this.Value = sensor.PreviousPublishedState;
if (sensor is WMIQuerySensor wMIQuerySensor)
{
this.Query = wMIQuerySensor.Query;
}
if (sensor is NamedWindowSensor namedWindowSensor)
{
this.WindowName = namedWindowSensor.WindowName;
}
this.UpdateInterval = sensor.UpdateInterval;
this.UnitOfMeasurement = ((SensorDiscoveryConfigModel)sensor.GetAutoDiscoveryConfig()).Unit_of_measurement;
}
public ConfiguredSensorModel()
{
}
}
public class ConfiguredCommandModel
{
public Guid Id { get; set; }
public string Type { get; set; }
public AvailableCommands Type { get; set; }
public string Name { get; set; }
public string Command { get; set; }
public string Key { get; set; }
public ConfiguredCommandModel(AbstractCommand command)
{
this.Id = command.Id;
Enum.TryParse(command.GetType().Name, out AvailableCommands type);
this.Type = type;
this.Name = command.Name;
if (command is CustomCommand customCommand)
{
this.Command = customCommand.Command;
}
if (command is KeyCommand keyCommand)
{
this.Key = "0x" + Convert.ToString(keyCommand.KeyCode, 16);
}
}
public ConfiguredCommandModel()
{
}
}
public enum AvailableSensors
{
UserNotificationStateSensor,
@ -52,6 +100,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
LastBootSensor,
SessionStateSensor,
CurrentVolumeSensor,
MasterVolumeSensor,
GPUTemperatureSensor,
GPULoadSensor
}
@ -62,6 +111,7 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
ShutdownCommand,
LogOffCommand,
RestartCommand,
HibernateCommand,
KeyCommand,
PlayPauseCommand,
NextCommand,
@ -70,4 +120,4 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
VolumeDownCommand,
MuteCommand
}
}
}

@ -31,6 +31,7 @@ namespace hass_workstation_service.Communication
public DateTime LastAvailabilityAnnounce { get; private set; }
public DeviceConfigModel DeviceConfigModel { get; private set; }
public ICollection<AbstractCommand> Subscribers { get; private set; }
public string NamePrefix { get; private set; }
public bool IsConnected
{
get
@ -55,9 +56,11 @@ namespace hass_workstation_service.Communication
this._logger = logger;
this.DeviceConfigModel = deviceConfigModel;
this._configurationService = configurationService;
this.NamePrefix = configurationService.GeneralSettings?.NamePrefix;
var options = _configurationService.GetMqttClientOptionsAsync().Result;
_configurationService.MqqtConfigChangedHandler = this.ReplaceMqttClient;
_configurationService.NamePrefixChangedHandler = this.UpdateNamePrefix;
var factory = new MqttFactory();
this._mqttClient = factory.CreateManagedMqttClient();
@ -96,9 +99,9 @@ namespace hass_workstation_service.Communication
this._logger.LogInformation($"Message dropped because mqtt not connected: {message}");
}
}
// TODO: This should take a sensor/command instead of a config.
// Then we can ask the sensor about the topic based on ObjectId instead of referencing Name directly
public async Task AnnounceAutoDiscoveryConfig(AbstractDiscoverable discoverable, string domain, bool clearConfig = false)
public async Task AnnounceAutoDiscoveryConfig(AbstractDiscoverable discoverable, bool clearConfig = false)
{
if (this._mqttClient.IsConnected)
{
@ -111,11 +114,21 @@ namespace hass_workstation_service.Communication
};
var message = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{domain}/{this.DeviceConfigModel.Name}/{discoverable.ObjectId}/config")
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/config")
.WithPayload(clearConfig ? "" : JsonSerializer.Serialize(discoverable.GetAutoDiscoveryConfig(), discoverable.GetAutoDiscoveryConfig().GetType(), options))
.WithRetainFlag()
.Build();
await this.Publish(message);
// if clearconfig is true, also remove previous state messages
if (clearConfig)
{
var stateMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/state")
.WithPayload("")
.WithRetainFlag()
.Build();
await this.Publish(stateMessage);
}
LastConfigAnnounce = DateTime.UtcNow;
}
}
@ -195,6 +208,11 @@ namespace hass_workstation_service.Communication
Subscribers.Add(command);
}
public void UpdateNamePrefix(string prefix)
{
this.NamePrefix = prefix;
}
private void HandleMessageReceived(MqttApplicationMessage applicationMessage)
{
foreach (AbstractCommand command in this.Subscribers)

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace hass_workstation_service.Communication
{
@ -14,12 +15,49 @@ namespace hass_workstation_service.Communication
/// (Optional) The name of the MQTT sensor. Defaults to MQTT Sensor in hass.
/// </summary>
/// <value></value>
[JsonIgnore]
public string Name { get; set; }
/// <summary>
/// (Optional) The first part of the name.
/// </summary>
/// <value></value>
[JsonIgnore]
public string NamePrefix { get; set; }
[JsonPropertyName("name")]
public string CompiledName { get => GetName(); }
/// <summary>
/// The MQTT topic subscribed to receive sensor values.
/// </summary>
/// <value></value>
public string State_topic { get; set; }
/// <summary>
/// Gets the name including the prefix
/// </summary>
/// <returns></returns>
public string GetName()
{
if (string.IsNullOrWhiteSpace(NamePrefix))
{
return Name;
}
return $"{NamePrefix}-{Name}";
}
/// <summary>
/// Gets the name including the prefix if the class has not been instantiated yet.
/// </summary>
/// <returns></returns>
public static string GetNameWithPrefix(string namePrefix, string name)
{
if (string.IsNullOrWhiteSpace(namePrefix))
{
return name;
}
return $"{namePrefix}-{name}";
}
}
public class SensorDiscoveryConfigModel : DiscoveryConfigModel
{

@ -26,36 +26,49 @@ namespace hass_workstation_service.Data
{
public ICollection<AbstractSensor> ConfiguredSensors { get; private set; }
public ICollection<AbstractCommand> ConfiguredCommands { get; private set; }
public GeneralSettings GeneralSettings { get; private set; }
public Action<IManagedMqttClientOptions> MqqtConfigChangedHandler { get; set; }
public Action<string> NamePrefixChangedHandler { get; set; }
private readonly DeviceConfigModel _deviceConfigModel;
private bool BrokerSettingsFileLocked { get; set; }
private bool SensorsSettingsFileLocked { get; set; }
private bool CommandSettingsFileLocked { get; set; }
private bool GeneralSettingsFileLocked { get; set; }
private bool _sensorsLoading { get; set; }
private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service");
private const string MQTT_SETTINGS_FILENAME = "mqttbroker.json";
private const string SENSORS_SETTINGS_FILENAME = "configured-sensors.json";
private const string COMMANDS_SETTINGS_FILENAME = "configured-commands.json";
private const string GENERAL_SETTINGS_FILENAME = "general-settings.json";
public ConfigurationService(DeviceConfigModel deviceConfigModel)
{
this._deviceConfigModel = deviceConfigModel;
if (!File.Exists(Path.Combine(path, "mqttbroker.json")))
if (!File.Exists(Path.Combine(path, MQTT_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, "mqttbroker.json")).Close();
File.Create(Path.Combine(path, MQTT_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, "configured-sensors.json")))
if (!File.Exists(Path.Combine(path, SENSORS_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, "configured-sensors.json")).Close();
File.Create(Path.Combine(path, SENSORS_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, "configured-commands.json")))
if (!File.Exists(Path.Combine(path, COMMANDS_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, COMMANDS_SETTINGS_FILENAME)).Close();
}
if (!File.Exists(Path.Combine(path, GENERAL_SETTINGS_FILENAME)))
{
File.Create(Path.Combine(path, "configured-commands.json")).Close();
File.Create(Path.Combine(path, GENERAL_SETTINGS_FILENAME)).Close();
}
ConfiguredSensors = new List<AbstractSensor>();
ConfiguredCommands = new List<AbstractCommand>();
this.ReadGeneralSettings();
}
public async void ReadSensorSettings(MqttPublisher publisher)
@ -67,7 +80,7 @@ namespace hass_workstation_service.Data
}
this.SensorsSettingsFileLocked = true;
List<ConfiguredSensor> sensors = new List<ConfiguredSensor>();
using (var stream = new FileStream(Path.Combine(path, "configured-sensors.json"), FileMode.Open))
using (var stream = new FileStream(Path.Combine(path, SENSORS_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured sensors from: {stream.Name}");
if (stream.Length > 0)
@ -125,6 +138,9 @@ namespace hass_workstation_service.Data
case "CurrentVolumeSensor":
sensor = new CurrentVolumeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "MasterVolumeSensor":
sensor = new MasterVolumeSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
case "GpuTemperatureSensor":
sensor = new GpuTemperatureSensor(publisher, configuredSensor.UpdateInterval, configuredSensor.Name, configuredSensor.Id);
break;
@ -155,7 +171,7 @@ namespace hass_workstation_service.Data
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> commands = new List<ConfiguredCommand>();
using (var stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
using (var stream = new FileStream(Path.Combine(path, COMMANDS_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured commands from: {stream.Name}");
if (stream.Length > 0)
@ -177,29 +193,32 @@ namespace hass_workstation_service.Data
case "RestartCommand":
command = new RestartCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "HibernateCommand":
command = new HibernateCommand(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);
command = new CustomCommand(publisher, configuredCommand.Command, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaPlayPauseCommand":
command = new MediaPlayPauseCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new PlayPauseCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaNextCommand":
command = new MediaNextCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new NextCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaPreviousCommand":
command = new MediaPreviousCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new PreviousCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaVolumeUpCommand":
command = new MediaVolumeUpCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new VolumeUpCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaVolumeDownCommand":
command = new MediaVolumeDownCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new VolumeDownCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "MediaMuteCommand":
command = new MediaMuteCommand(publisher, configuredCommand.Name, configuredCommand.Id);
command = new MuteCommand(publisher, configuredCommand.Name, configuredCommand.Id);
break;
case "KeyCommand":
command = new KeyCommand(publisher, configuredCommand.KeyCode, configuredCommand.Name, configuredCommand.Id);
@ -215,6 +234,70 @@ namespace hass_workstation_service.Data
}
}
public async Task<GeneralSettings> ReadGeneralSettings()
{
while (this.GeneralSettingsFileLocked)
{
await Task.Delay(500);
}
this.GeneralSettingsFileLocked = true;
GeneralSettings settings = new GeneralSettings();
using (var stream = new FileStream(Path.Combine(path, GENERAL_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading general settings from: {stream.Name}");
if (stream.Length > 0)
{
settings = await JsonSerializer.DeserializeAsync<GeneralSettings>(stream);
}
stream.Close();
this.GeneralSettings = settings;
this.GeneralSettingsFileLocked = false;
return settings;
}
}
/// <summary>
/// Writes provided settings to the config file and reconfigures all sensors and commands if the nameprefix changed
/// </summary>
/// <param name="settings"></param>
public async void WriteGeneralSettingsAsync(GeneralSettings settings)
{
while (this.GeneralSettingsFileLocked)
{
await Task.Delay(500);
}
this.GeneralSettingsFileLocked = true;
using (FileStream stream = new FileStream(Path.Combine(path, GENERAL_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing general settings to: {stream.Name}");
await JsonSerializer.SerializeAsync(stream, settings);
stream.Close();
}
this.GeneralSettingsFileLocked = false;
// if the nameprefix changed, we need to update all sensors and commands to reflect the new name
if (settings.NamePrefix != this.GeneralSettings.NamePrefix)
{
// notify the mqtt publisher of the new prefix
this.NamePrefixChangedHandler.Invoke(settings.NamePrefix);
foreach (AbstractSensor sensor in this.ConfiguredSensors)
{
await sensor.UnPublishAutoDiscoveryConfigAsync();
sensor.PublishAutoDiscoveryConfigAsync();
}
foreach (AbstractCommand command in this.ConfiguredCommands)
{
await command.UnPublishAutoDiscoveryConfigAsync();
command.PublishAutoDiscoveryConfigAsync();
}
}
}
public async Task<IManagedMqttClientOptions> GetMqttClientOptionsAsync()
{
ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync();
@ -226,7 +309,8 @@ namespace hass_workstation_service.Data
.WithTls(new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
})
.WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString())
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
@ -257,7 +341,7 @@ namespace hass_workstation_service.Data
}
this.BrokerSettingsFileLocked = true;
ConfiguredMqttBroker configuredBroker = null;
using (FileStream stream = new FileStream(Path.Combine(path, "mqttbroker.json"), FileMode.Open))
using (FileStream stream = new FileStream(Path.Combine(path, MQTT_SETTINGS_FILENAME), FileMode.Open))
{
Log.Logger.Information($"reading configured mqttbroker from: {stream.Name}");
if (stream.Length > 0)
@ -279,7 +363,7 @@ namespace hass_workstation_service.Data
}
this.SensorsSettingsFileLocked = true;
List<ConfiguredSensor> configuredSensorsToSave = new List<ConfiguredSensor>();
using (FileStream stream = new FileStream(Path.Combine(path, "configured-sensors.json"), FileMode.Open))
using (FileStream stream = new FileStream(Path.Combine(path, SENSORS_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured sensors to: {stream.Name}");
@ -316,7 +400,7 @@ namespace hass_workstation_service.Data
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> configuredCommandsToSave = new List<ConfiguredCommand>();
using (FileStream stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
using (FileStream stream = new FileStream(Path.Combine(path, COMMANDS_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured commands to: {stream.Name}");
@ -340,54 +424,96 @@ namespace hass_workstation_service.Data
public void AddConfiguredSensor(AbstractSensor sensor)
{
this.ConfiguredSensors.Add(sensor);
sensor.PublishAutoDiscoveryConfigAsync();
AddSensor(sensor);
WriteSensorSettingsAsync();
}
public void AddConfiguredCommand(AbstractCommand command)
{
this.ConfiguredCommands.Add(command);
command.PublishAutoDiscoveryConfigAsync();
AddCommand(command);
WriteCommandSettingsAsync();
}
public async void DeleteConfiguredSensor(Guid id)
public void AddConfiguredSensors(List<AbstractSensor> sensors)
{
var sensorToRemove = this.ConfiguredSensors.FirstOrDefault(s => s.Id == id);
if (sensorToRemove != null)
{
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
this.ConfiguredSensors.Remove(sensorToRemove);
WriteSensorSettingsAsync();
}
else
{
Log.Logger.Warning($"sensor with id {id} not found");
}
sensors.ForEach(sensor => AddSensor(sensor));
WriteSensorSettingsAsync();
}
public void AddConfiguredCommands(List<AbstractCommand> commands)
{
commands.ForEach(command => AddCommand(command));
WriteCommandSettingsAsync();
}
public async void DeleteConfiguredSensor(Guid id)
{
await DeleteSensor(id);
WriteSensorSettingsAsync();
}
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
await DeleteCommand(id);
WriteCommandSettingsAsync();
}
/// <summary>
///
/// </summary>
/// <param name="id">The Id of the sensor to replace</param>
/// <param name="sensor">The new sensor</param>
public async void UpdateConfiguredSensor(Guid id, AbstractSensor sensor)
{
await DeleteSensor(id);
await Task.Delay(500);
AddSensor(sensor);
WriteSensorSettingsAsync();
}
public async void UpdateConfiguredCommand(Guid id, AbstractCommand command)
{
await DeleteCommand(id);
await Task.Delay(500);
AddCommand(command);
WriteCommandSettingsAsync();
}
private void AddSensor(AbstractSensor sensor)
{
ConfiguredSensors.Add(sensor);
sensor.PublishAutoDiscoveryConfigAsync();
}
private void AddCommand(AbstractCommand command)
{
ConfiguredCommands.Add(command);
command.PublishAutoDiscoveryConfigAsync();
}
private async Task DeleteSensor(Guid id)
{
var sensorToRemove = ConfiguredSensors.FirstOrDefault(s => s.Id == id);
if (sensorToRemove == null)
{
Log.Logger.Warning($"command with id {id} not found");
Log.Logger.Warning($"sensor with id {id} not found");
return;
}
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
ConfiguredSensors.Remove(sensorToRemove);
}
public void AddConfiguredSensors(List<AbstractSensor> sensors)
private async Task DeleteCommand(Guid id)
{
sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor));
WriteSensorSettingsAsync();
var commandToRemove = ConfiguredCommands.FirstOrDefault(c => c.Id == id);
if (commandToRemove == null)
{
Log.Logger.Warning($"command with id {id} not found");
return;
}
await commandToRemove.UnPublishAutoDiscoveryConfigAsync();
ConfiguredCommands.Remove(commandToRemove);
}
/// <summary>
@ -401,7 +527,7 @@ namespace hass_workstation_service.Data
await Task.Delay(500);
}
this.BrokerSettingsFileLocked = true;
using (FileStream stream = new FileStream(Path.Combine(path, "mqttbroker.json"), FileMode.Open))
using (FileStream stream = new FileStream(Path.Combine(path, MQTT_SETTINGS_FILENAME), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured mqttbroker to: {stream.Name}");

@ -0,0 +1,13 @@
using hass_workstation_service.Domain.Sensors;
using System;
namespace hass_workstation_service.Data
{
public class GeneralSettings
{
/// <summary>
/// If set, all sensor and command names will be be prefixed with this
/// </summary>
public string NamePrefix { get; set; }
}
}

@ -13,13 +13,21 @@ namespace hass_workstation_service.Data
{
public interface IConfigurationService
{
ICollection<AbstractSensor> ConfiguredSensors { get; }
Action<IManagedMqttClientOptions> MqqtConfigChangedHandler { get; set; }
Action<string> NamePrefixChangedHandler { get; set; }
ICollection<AbstractSensor> ConfiguredSensors { get; }
ICollection<AbstractCommand> ConfiguredCommands { get; }
GeneralSettings GeneralSettings { get; }
void AddConfiguredCommand(AbstractCommand command);
void AddConfiguredSensor(AbstractSensor sensor);
void AddConfiguredCommand(AbstractCommand command);
void AddConfiguredSensors(List<AbstractSensor> sensors);
void AddConfiguredCommands(List<AbstractCommand> commands);
void DeleteConfiguredSensor(Guid id);
void DeleteConfiguredCommand(Guid id);
Task<GeneralSettings> ReadGeneralSettings();
void UpdateConfiguredSensor(Guid id, AbstractSensor sensor);
void UpdateConfiguredCommand(Guid id, AbstractCommand command);
Task<IManagedMqttClientOptions> GetMqttClientOptionsAsync();
void ReadSensorSettings(MqttPublisher publisher);
void WriteMqttBrokerSettingsAsync(MqttSettings settings);
@ -27,10 +35,9 @@ namespace hass_workstation_service.Data
Task<MqttSettings> GetMqttBrokerSettings();
void EnableAutoStart(bool enable);
bool IsAutoStartEnabled();
void DeleteConfiguredSensor(Guid id);
void DeleteConfiguredCommand(Guid id);
void WriteCommandSettingsAsync();
void ReadCommandSettings(MqttPublisher publisher);
Task<ICollection<AbstractSensor>> GetSensorsAfterLoadingAsync();
void WriteGeneralSettingsAsync(GeneralSettings settings);
}
}

@ -12,16 +12,8 @@ namespace hass_workstation_service.Domain
{
public abstract string Domain { get; }
public string Name { get; protected set; }
public string ObjectId
{
get
{
return Regex.Replace(this.Name, "[^a-zA-Z0-9_-]", "_");
}
}
public string ObjectId => Regex.Replace(Name, "[^a-zA-Z0-9_-]", "_");
public Guid Id { get; protected set; }
public abstract DiscoveryConfigModel GetAutoDiscoveryConfig();
}
}
}

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

@ -30,6 +30,11 @@ namespace hass_workstation_service.Domain.Commands
startInfo.FileName = "cmd.exe";
startInfo.Arguments = $"/C {this.Command}";
this.Process.StartInfo = startInfo;
// turn off the sensor to guarantee disable the switch
// useful if command changes power state of device
this.State = "OFF";
try
{
this.Process.Start();
@ -39,12 +44,6 @@ namespace hass_workstation_service.Domain.Commands
Log.Logger.Error($"Sensor {this.Name} failed", e);
this.State = "FAILED";
}
while (!this.Process.HasExited)
{
await Task.Delay(1000);
}
this.State = "OFF";
}
@ -54,10 +53,11 @@ namespace hass_workstation_service.Domain.Commands
return new CommandDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/set",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{Publisher.NamePrefix}{this.ObjectId}/set",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Device = this.Publisher.DeviceConfigModel,
};
}

@ -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 HibernateCommand : CustomCommand
{
public HibernateCommand(MqttPublisher publisher, string name = "Hibernate", Guid id = default(Guid)) : base(publisher, "shutdown /h", name ?? "Hibernate", id)
{
this.State = "OFF";
}
}
}

@ -11,7 +11,7 @@ namespace hass_workstation_service.Domain.Commands
public class KeyCommand : AbstractCommand
{
public const int KEYEVENTF_EXTENTEDKEY = 1;
public const int KEYEVENTF_KEYUP = 0;
public const int KEYEVENTF_KEYUP = 0x0002;
public const int VK_MEDIA_NEXT_TRACK = 0xB0;
public const int VK_MEDIA_PLAY_PAUSE = 0xB3;
public const int VK_MEDIA_PREV_TRACK = 0xB1;
@ -30,10 +30,11 @@ namespace hass_workstation_service.Domain.Commands
return new CommandDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/set",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{Publisher.NamePrefix}{this.ObjectId}/set",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Device = this.Publisher.DeviceConfigModel,
};
}
@ -54,7 +55,8 @@ namespace hass_workstation_service.Domain.Commands
public override void TurnOn()
{
keybd_event(this.KeyCode, 0, KEYEVENTF_EXTENTEDKEY, IntPtr.Zero);
keybd_event(this.KeyCode, 0, 0, IntPtr.Zero);
keybd_event(this.KeyCode, 0, KEYEVENTF_KEYUP, IntPtr.Zero);
}
}
}

@ -1,14 +0,0 @@
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 MediaPlayPauseCommand : KeyCommand
{
public MediaPlayPauseCommand(MqttPublisher publisher, string name = "PlayPause", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_PLAY_PAUSE, name ?? "PlayPause", id) { }
}
}

@ -1,14 +0,0 @@
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 MediaPreviousCommand : KeyCommand
{
public MediaPreviousCommand(MqttPublisher publisher, string name = "Previous", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_PREV_TRACK, name ?? "Previous", id) { }
}
}

@ -1,14 +0,0 @@
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 MediaVolumeDownCommand : KeyCommand
{
public MediaVolumeDownCommand(MqttPublisher publisher, string name = "VolumeDown", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_DOWN, name ?? "VolumeDown", id) { }
}
}

@ -1,14 +0,0 @@
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 MediaVolumeUpCommand : KeyCommand
{
public MediaVolumeUpCommand(MqttPublisher publisher, string name = "VolumeUp", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_UP, name ?? "VolumeUp", id) { }
}
}

@ -7,8 +7,8 @@ using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class MediaMuteCommand : KeyCommand
public class MuteCommand : KeyCommand
{
public MediaMuteCommand(MqttPublisher publisher, string name = "Mute", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_MUTE, name ?? "Mute", id) { }
public MuteCommand(MqttPublisher publisher, string name = "Mute", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_MUTE, name ?? "Mute", id) { }
}
}

@ -7,8 +7,8 @@ using System.Threading.Tasks;
namespace hass_workstation_service.Domain.Commands
{
public class MediaNextCommand : KeyCommand
public class NextCommand : KeyCommand
{
public MediaNextCommand(MqttPublisher publisher, string name = "Next", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_NEXT_TRACK, name ?? "Next", id) { }
public NextCommand(MqttPublisher publisher, string name = "Next", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_NEXT_TRACK, name ?? "Next", id) { }
}
}

@ -0,0 +1,14 @@
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 PlayPauseCommand : KeyCommand
{
public PlayPauseCommand(MqttPublisher publisher, string name = "PlayPause", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_PLAY_PAUSE, name ?? "PlayPause", id) { }
}
}

@ -0,0 +1,14 @@
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 PreviousCommand : KeyCommand
{
public PreviousCommand(MqttPublisher publisher, string name = "Previous", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_MEDIA_PREV_TRACK, name ?? "Previous", id) { }
}
}

@ -0,0 +1,14 @@
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 VolumeDownCommand : KeyCommand
{
public VolumeDownCommand(MqttPublisher publisher, string name = "VolumeDown", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_DOWN, name ?? "VolumeDown", id) { }
}
}

@ -0,0 +1,14 @@
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 VolumeUpCommand : KeyCommand
{
public VolumeUpCommand(MqttPublisher publisher, string name = "VolumeUp", Guid id = default(Guid)) : base(publisher, KeyCommand.VK_VOLUME_UP, name ?? "VolumeUp", id) { }
}
}

@ -1,12 +1,10 @@
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using hass_workstation_service.Communication;
using MQTTnet;
namespace hass_workstation_service.Domain.Sensors
{
public abstract class AbstractSensor : AbstractDiscoverable
{
/// <summary>
@ -17,61 +15,52 @@ namespace hass_workstation_service.Domain.Sensors
public string PreviousPublishedState { 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))
{
if (id == Guid.Empty)
{
this.Id = Guid.NewGuid();
}
else
{
this.Id = id;
}
this.Name = name;
this.Publisher = publisher;
this.UpdateInterval = updateInterval;
}
protected SensorDiscoveryConfigModel _autoDiscoveryConfigModel;
protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config)
public AbstractSensor(MqttPublisher publisher, string name, int updateInterval = 10, Guid id = default)
{
this._autoDiscoveryConfigModel = config;
return config;
Id = id == Guid.Empty ? Guid.NewGuid() : id;
Name = name;
Publisher = publisher;
UpdateInterval = updateInterval;
}
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
// dont't even check the state if the update interval hasn't passed
if (LastUpdated.HasValue && LastUpdated.Value.AddSeconds(UpdateInterval) > DateTime.UtcNow)
return;
}
string state = this.GetState();
if (this.PreviousPublishedState == state)
{
// don't publish the state if it hasn't changed
string state = GetState();
// don't publish the state if it hasn't changed
if (PreviousPublishedState == state)
return;
}
var message = new MqttApplicationMessageBuilder()
.WithTopic(this.GetAutoDiscoveryConfig().State_topic)
.WithPayload(state)
.WithExactlyOnceQoS()
.WithRetainFlag()
.Build();
.WithTopic(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, this.Domain);
PreviousPublishedState = state;
LastUpdated = DateTime.UtcNow;
}
public async void PublishAutoDiscoveryConfigAsync() => await Publisher.AnnounceAutoDiscoveryConfig(this);
public async Task UnPublishAutoDiscoveryConfigAsync()
{
await this.Publisher.AnnounceAutoDiscoveryConfig(this, this.Domain, true);
await Publisher.AnnounceAutoDiscoveryConfig(this, true);
this._autoDiscoveryConfigModel = null;
}
protected SensorDiscoveryConfigModel _autoDiscoveryConfigModel;
protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config)
{
_autoDiscoveryConfigModel = config;
return config;
}
}
}

@ -14,9 +14,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:window-maximize",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});

@ -22,9 +22,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:chart-areaspline",
Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"

@ -14,9 +14,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:speedometer",
Unit_of_measurement = "MHz",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"

@ -23,9 +23,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:volume-medium",
Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"

@ -18,9 +18,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});
}

@ -35,9 +35,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});

@ -35,9 +35,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Device_class = "temperature",
Unit_of_measurement = "°C",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"

@ -14,11 +14,11 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:clock-time-three-outline",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability",
Device_class = "timestamp"
});
}

@ -18,11 +18,11 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:clock-time-three-outline",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability",
Device_class = "timestamp"
});
}

@ -0,0 +1,41 @@
using CoreAudio;
using hass_workstation_service.Communication;
using System;
using System.Globalization;
namespace hass_workstation_service.Domain.Sensors
{
public class MasterVolumeSensor : AbstractSensor
{
private MMDeviceEnumerator deviceEnumerator;
public MasterVolumeSensor(MqttPublisher publisher, int? updateInterval = null, string name = "MasterVolume", Guid id = default(Guid)) : base(publisher, name ?? "CurrentVolume", updateInterval ?? 10, id)
{
this.deviceEnumerator = new MMDeviceEnumerator();
}
public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
Icon = "mdi:volume-medium",
Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});
}
public override string GetState()
{
var defaultAudioDevice = deviceEnumerator.GetDefaultAudioEndpoint(EDataFlow.eRender, ERole.eMultimedia);
// check if the volume is muted
if (defaultAudioDevice.AudioEndpointVolume.Mute) return "0";
return Math.Round(defaultAudioDevice.AudioEndpointVolume.MasterVolumeLevelScalar * 100, 0) // round volume and convert to percent
.ToString(CultureInfo.InvariantCulture); // convert to string
}
}
}

@ -42,9 +42,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:memory",
Unit_of_measurement = "%",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"

@ -27,9 +27,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability"
});
}

@ -22,11 +22,12 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability"
});
});;
}
public override string GetState()

@ -42,9 +42,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.ObjectId}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:lock",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});

@ -14,9 +14,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Icon = "mdi:laptop",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});

@ -25,9 +25,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability"
});
}

@ -30,9 +30,10 @@ namespace hass_workstation_service.Domain.Sensors
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{
Name = this.Name,
NamePrefix = Publisher.NamePrefix,
Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(Publisher.NamePrefix, this.ObjectId)}/state",
Availability_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/availability"
});
}

@ -70,7 +70,6 @@ namespace hass_workstation_service
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((hostContext, loggingBuilder) =>
loggingBuilder.AddSerilog(dispose: true))
.ConfigureServices((hostContext, services) =>
@ -84,14 +83,14 @@ namespace hass_workstation_service
Sw_version = GetVersion()
};
services.AddSingleton(deviceConfig);
services.AddSingleton<ServiceContractInterfaces, InterProcessApi>();
services.AddSingleton<IServiceContractInterfaces, InterProcessApi>();
services.AddSingleton<IConfigurationService, ConfigurationService>();
services.AddSingleton<MqttPublisher>();
services.AddHostedService<Worker>();
}).ConfigureIpcHost(builder =>
{
// configure IPC endpoints
builder.AddNamedPipeEndpoint<ServiceContractInterfaces>(pipeName: "pipeinternal");
builder.AddNamedPipeEndpoint<IServiceContractInterfaces>(pipeName: "pipeinternal");
});
static internal string 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>47</ApplicationRevision>
<ApplicationRevision>55</ApplicationRevision>
<ApplicationVersion>1.0.0.*</ApplicationVersion>
<BootstrapperEnabled>True</BootstrapperEnabled>
<Configuration>Release</Configuration>
@ -37,6 +37,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<TrustUrlParameters>True</TrustUrlParameters>
<UpdateEnabled>True</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<History>False|2021-11-14T15:44:38.1032015Z;</History>
</PropertyGroup>
<ItemGroup>
<BootstrapperPackage Include="Microsoft.NetCore.CoreRuntime.5.0.x64">

@ -1,7 +0,0 @@
{
"MqttBroker": {
"Host": "192.168.2.6",
"Username": "tester",
"Password": "tester"
}
}

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Worker">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
@ -42,6 +42,9 @@
<Content Include="UserInterface.exe">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="UserInterface.pdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
@ -52,14 +55,14 @@
<ItemGroup>
<PackageReference Include="JKang.IpcServiceFramework.Hosting.NamedPipe" Version="3.1.0" />
<PackageReference Include="LibreHardwareMonitorLib" Version="0.8.7" />
<PackageReference Include="LibreHardwareMonitorLib" Version="0.8.8" />
<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="MQTTnet.Extensions.ManagedClient" Version="3.0.13" />
<PackageReference Include="MQTTnet" Version="3.0.16" />
<PackageReference Include="MQTTnet.Extensions.ManagedClient" Version="3.0.16" />
<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="Serilog.Sinks.Console" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Management" Version="5.0.0" />
</ItemGroup>
@ -67,5 +70,11 @@
<Reference Include="CoreAudio">
<HintPath>..\lib\CoreAudio.dll</HintPath>
</Reference>
<!--<Reference Include="libHarfBuzzSharp.dll">
<HintPath>..\lib\libHarfBuzzSharp.dll</HintPath>
</Reference>
<Reference Include="libSkiaSharp.dll">
<HintPath>..\lib\libSkiaSharp.dll</HintPath>
</Reference>-->
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
{
"runtimeOptions": {
"tfm": "net5.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "5.0.0"
}
}
}
Loading…
Cancel
Save