Merge pull request #151 from prestomation/master

[WIP]Add support for Client Certificates for MQTT and disabling Retained messages
pull/158/head
sleevezipper 3 years ago committed by GitHub
commit 77cf49fcb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,13 +13,25 @@ It will try to futher accomplish this goal in the future by:
- Being platform independent - Being platform independent
## Screenshots ## Hass Workstation Service in the press
<details>
<summary>View some screenshots</summary>
![The settings screen](https://i.imgur.com/RBQx807.png) ![The settings screen](https://i.imgur.com/RBQx807.png)
![The resulting sensors and commands in Home Assistant](https://i.imgur.com/jXRU2cu.png) ![The resulting sensors and commands in Home Assistant](https://i.imgur.com/jXRU2cu.png)
Not convinced yet? Check out [this excellent video](https://youtu.be/D5A7le79R5M) by GeekToolkit on YouTube. </details>
If you would prefer a video, look at :
- [How to Control a PC from Home Assistant](https://youtu.be/D5A7le79R5M) by GeekToolkit on YouTube.
- [Mit Home-Assistant den Computer AN und AUS schalten!](https://www.youtube.com/watch?v=oDJHGEcV84A) by Fabsenet on YouTube.
- [The Butter, What?! show's review](https://youtu.be/wBTKfwkV-vs?t=376) by Pat and Brian on youtube.
Or a written article :
- [How to Setup HASS Workstation Service in Home Assistant](https://smarthomepursuits.com/how-to-setup-hass-workstation-service-in-home-assistant/) by Danny @ smarthomepursuits.com
- [Control your Windows PC with HASS Workstation Service](https://home-assistant-guide.com/2021/01/18/control-your-windows-pc-with-hass-workstation-service/) by home-assistant-guide.com
## Installation ## Installation
@ -35,7 +47,7 @@ If you don't want to use the installer, standalone is what you need. Make sure t
As a prerequisite, make sure you have an MQTT username and password available. Using Home Assistant in combination with the Mosquitto broker add-on and integration? You can both use a Home Assistant account and a local account. From a security perspective, we recommend a local account as this only provides access to the MQTT Broker and not to your Home Assistant instance. As a prerequisite, make sure you have an MQTT username and password available. Using Home Assistant in combination with the Mosquitto broker add-on and integration? You can both use a Home Assistant account and a local account. From a security perspective, we recommend a local account as this only provides access to the MQTT Broker and not to your Home Assistant instance.
Now that you are all set, make sure to run the `hass-workstation-service.exe` executable first. This executable is responsible for setting up the sensors and talking with your MQTT Broker. To configure the service, start the `UserInterface.exe` executable. Now that you are all set, make sure to run the `hass-workstation-service.exe` executable first. This executable is responsible for setting up the sensors and talking with your MQTT Broker. To configure the service, start the `UserInterface.exe` executable.
Add your `hostname` or `IP address`, `port`, `username` and `password` and click on Save. In case you use the Mosquitto add-in, provide port `8883` and check `Use TLS`. The application will mention "All good" when configured correctly. Add your `hostname` or `IP address`, `port`, `username` and `password` and click on Save. In case you use the Mosquitto add-in, provide port `8883` and check `Use TLS`. If you don't want to use TLS the default port is `1883`. The application will mention "All good" when configured correctly.
### Updating ### Updating
@ -43,7 +55,7 @@ If you used the installer, the app checks for updates on startup. If an update i
## Need help? ## Need help?
Find us on [Discord](https://discord.gg/VraYT2N3wd). Find us on [Discord](https://discord.gg/VraYT2N3wd), or check out the [frequently asked questions](https://github.com/sleevezipper/hass-workstation-service/blob/master/documentation/FAQ.md#frequently-asked-questions).
## Development ## Development
@ -67,8 +79,8 @@ Here is a list of the most commonly used sensors with the full documentation [he
|sensor|use| |sensor|use|
|---|---| |---|---|
|ActiveWindow|Exposes the currently selected window| |ActiveWindow|Exposes the currently selected window|
|WebcamActive|Exposes the microphone state| |WebcamActive|Exposes the webcam state|
|MicrophoneActive|Exposes the webcam state| |MicrophoneActive|Exposes the microphone state|
## Commands ## Commands

@ -16,6 +16,9 @@ namespace UserInterface.ViewModels
private bool isConnected; private bool isConnected;
private int? port; private int? port;
private bool useTLS; private bool useTLS;
private bool retainLWT = true;
private string rootCaPath;
private string clientCertPath;
public bool IsConnected { get => isConnected; set => this.RaiseAndSetIfChanged(ref isConnected, value); } public bool IsConnected { get => isConnected; set => this.RaiseAndSetIfChanged(ref isConnected, value); }
public string Message { get => message; set => this.RaiseAndSetIfChanged(ref message, value); } public string Message { get => message; set => this.RaiseAndSetIfChanged(ref message, value); }
@ -29,6 +32,13 @@ namespace UserInterface.ViewModels
public bool UseTLS { get => useTLS; set => this.RaiseAndSetIfChanged(ref useTLS, value); } public bool UseTLS { get => useTLS; set => this.RaiseAndSetIfChanged(ref useTLS, value); }
public bool RetainLWT { get => retainLWT; set => this.RaiseAndSetIfChanged(ref retainLWT, value); }
public string RootCAPath { get => rootCaPath; set => this.RaiseAndSetIfChanged(ref rootCaPath, value); }
public string ClientCertPath { get => clientCertPath; set => this.RaiseAndSetIfChanged(ref clientCertPath, value); }
public void Update(MqttSettings settings) public void Update(MqttSettings settings)
{ {
this.Host = settings.Host; this.Host = settings.Host;
@ -36,6 +46,9 @@ namespace UserInterface.ViewModels
this.Password = settings.Password; this.Password = settings.Password;
this.Port = settings.Port; this.Port = settings.Port;
this.UseTLS = settings.UseTLS; this.UseTLS = settings.UseTLS;
this.RetainLWT = settings.RetainLWT;
this.RootCAPath = settings.RootCAPath;
this.ClientCertPath = settings.ClientCertPath;
} }
public void UpdateStatus(MqqtClientStatus status) public void UpdateStatus(MqqtClientStatus status)

@ -19,12 +19,69 @@
<StackPanel Orientation="Vertical" Margin="30 0 0 0"> <StackPanel Orientation="Vertical" Margin="30 0 0 0">
<ContentControl Margin="0 20 0 10">Use TLS</ContentControl> <ContentControl Margin="0 20 0 10">Use TLS</ContentControl>
<CheckBox IsChecked="{Binding UseTLS}" HorizontalAlignment="Left" Margin="0 3 0 0"/> <CheckBox IsChecked="{Binding UseTLS}" HorizontalAlignment="Left" Margin="0 3 0 0"/>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Retain LastWillAndTestament</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, sets Retain on the Last Will and Testament message.
Only turn this off if you use a broker that does not support this(e.g. AWS IoT Core)
Defaults to True
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<CheckBox IsChecked="{Binding RetainLWT}" HorizontalAlignment="Left" Margin="0 3 0 0"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<ContentControl Margin="0 20 0 10">Username</ContentControl> <ContentControl Margin="0 20 0 10">Username</ContentControl>
<TextBox Text="{Binding Username}" MinWidth="150"/> <TextBox Text="{Binding Username}" MinWidth="150"/>
<ContentControl Margin="0 20 0 10">Password</ContentControl> <ContentControl Margin="0 20 0 10">Password</ContentControl>
<TextBox Text="{Binding Password}" MinWidth="150" PasswordChar="•"/> <TextBox Text="{Binding Password}" MinWidth="150" PasswordChar="•"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Root Cert Path (.pem/.crt)</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, use this certificate in the TLS configuration for the MQTT connection.
This will be a pem or crt file provided by your broker.
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding RootCAPath}" MinWidth="150"/>
<StackPanel Margin="0 20 0 10" HorizontalAlignment="Left" Orientation="Horizontal">
<ContentControl>Client Cert Path (.pfx)</ContentControl>
<TextBlock Cursor="Help" Margin="5 0 0 0" VerticalAlignment="Bottom" TextDecorations="Underline">
(What's this?)
<ToolTip.Tip>
<StackPanel>
<TextBlock>
[Experimental]
If set, use this certificate in the TLS configuration for the MQTT connection.
This should be the private key .pfx file for a device created in your broker corresponding to this Windows PC.
</TextBlock>
</StackPanel>
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<TextBox Text="{Binding ClientCertPath}" MinWidth="150"/>
<Button Width="75" HorizontalAlignment="Right" Margin="0 40 0 10" Click="Configure">Save</Button>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

@ -50,7 +50,7 @@ namespace UserInterface.Views
ICollection<ValidationResult> results; ICollection<ValidationResult> results;
if (model.IsValid(model, out results)) if (model.IsValid(model, out results))
{ {
var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS })); var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password ?? "", Port = model.Port, UseTLS = model.UseTLS, RootCAPath = model.RootCAPath, ClientCertPath = model.ClientCertPath, RetainLWT = model.RetainLWT }));
} }
} }

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

@ -0,0 +1,25 @@
# Frequently asked questions
There are some common problems people encounter with hass workstations service, so if you run into a problem you should search this list. Most browsers have a search function you can access by pressing `CTRL` and `F` simultaniously.
If you cannot solve your problem still, join the [discord server](https://discord.gg/VraYT2N3wd).
### Where are config files located?
You can find the configuration files inside of `%appdata%\Hass Workstation Service` on windows.
|file|usage|
| --- | --- |
|configured-commands.json|stores all data about commands, including their properties|
|configured-sensors.json|stores information about sensors, including their properties|
|mqttbroker.json|stores data about your MQTT broker, dont share online|
### Are there any client logs?
Check the logs folder, its stored in the same place as config files.
### I cannot find documentation on a new feature?
You can submit a pull request with new documentation, ask on the [discord server](https://discord.gg/VraYT2N3wd), or check the [develop branch](https://github.com/sleevezipper/hass-workstation-service/tree/develop) for updated documentation.

@ -11,6 +11,10 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
public string Password { get; set; } public string Password { get; set; }
public int? Port { get; set; } public int? Port { get; set; }
public bool UseTLS { get; set; } public bool UseTLS { get; set; }
public bool RetainLWT { get; set; }
public string RootCAPath { get; set; }
public string ClientCertPath { get; set; }
} }
public class MqqtClientStatus public class MqqtClientStatus

@ -116,7 +116,7 @@ namespace hass_workstation_service.Communication
var message = new MqttApplicationMessageBuilder() var message = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, 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)) .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(discoverable.GetAutoDiscoveryConfig(), discoverable.GetAutoDiscoveryConfig().GetType(), options))
.WithRetainFlag() //.WithRetainFlag()
.Build(); .Build();
await this.Publish(message); await this.Publish(message);
// if clearconfig is true, also remove previous state messages // if clearconfig is true, also remove previous state messages
@ -125,7 +125,7 @@ namespace hass_workstation_service.Communication
var stateMessage = new MqttApplicationMessageBuilder() var stateMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/state") .WithTopic($"homeassistant/{discoverable.Domain}/{this.DeviceConfigModel.Name}/{DiscoveryConfigModel.GetNameWithPrefix(discoverable.GetAutoDiscoveryConfig().NamePrefix, discoverable.ObjectId)}/state")
.WithPayload("") .WithPayload("")
.WithRetainFlag() // .WithRetainFlag()
.Build(); .Build();
await this.Publish(stateMessage); await this.Publish(stateMessage);
} }

@ -5,6 +5,7 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using System.Security; using System.Security;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
@ -20,6 +21,7 @@ using MQTTnet.Client.Options;
using MQTTnet.Extensions.ManagedClient; using MQTTnet.Extensions.ManagedClient;
using Serilog; using Serilog;
namespace hass_workstation_service.Data namespace hass_workstation_service.Data
{ {
public class ConfigurationService : IConfigurationService public class ConfigurationService : IConfigurationService
@ -310,22 +312,54 @@ namespace hass_workstation_service.Data
if (configuredBroker != null && configuredBroker.Host != null) if (configuredBroker != null && configuredBroker.Host != null)
{ {
var mqttClientOptions = new MqttClientOptionsBuilder()
var mqttClientOptionsBuilder = new MqttClientOptionsBuilder()
.WithTcpServer(configuredBroker.Host, configuredBroker.Port) .WithTcpServer(configuredBroker.Host, configuredBroker.Port)
.WithTls(new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
})
.WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString()) .WithCredentials(configuredBroker.Username, configuredBroker.Password.ToString())
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30)) .WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
.WithWillMessage(new MqttApplicationMessageBuilder()
.WithRetainFlag()
/* Start LWT */
var lwtMessage = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability") .WithTopic($"homeassistant/sensor/{_deviceConfigModel.Name}/availability")
.WithPayload("offline") .WithPayload("offline");
.Build()) if (configuredBroker.RetainLWT) {
.Build(); lwtMessage.WithRetainFlag();
}
mqttClientOptionsBuilder.WithWillMessage(lwtMessage.Build());
/* End LWT */
/* Start TLS/Certificate configuration */
var tlsParameters = new MqttClientOptionsBuilderTlsParameters()
{
UseTls = configuredBroker.UseTLS,
AllowUntrustedCertificates = true,
SslProtocol = configuredBroker.UseTLS ? System.Security.Authentication.SslProtocols.Tls12 : System.Security.Authentication.SslProtocols.None
};
var certs = new List<X509Certificate>();
if (configuredBroker.RootCAPath != null) {
certs.Add(new X509Certificate2(configuredBroker.RootCAPath));
}
if (configuredBroker.ClientCertPath != null)
{
certs.Add(new X509Certificate2(configuredBroker.ClientCertPath));
}
if (certs.Count > 0) {
// IF certs are configured, let's add them here
tlsParameters.Certificates = certs;
}
mqttClientOptionsBuilder.WithTls(tlsParameters);
/* End TLS/Certificate Configuration */
var mqttClientOptions = mqttClientOptionsBuilder.Build();
return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build(); return new ManagedMqttClientOptionsBuilder().WithClientOptions(mqttClientOptions).Build();
} }
else else
@ -544,7 +578,10 @@ namespace hass_workstation_service.Data
Username = settings.Username, Username = settings.Username,
Password = settings.Password ?? "", Password = settings.Password ?? "",
Port = settings.Port ?? 1883, Port = settings.Port ?? 1883,
UseTLS = settings.UseTLS UseTLS = settings.UseTLS,
RetainLWT = settings.RetainLWT,
RootCAPath = settings.RootCAPath,
ClientCertPath = settings.ClientCertPath
}; };
await JsonSerializer.SerializeAsync(stream, configuredBroker); await JsonSerializer.SerializeAsync(stream, configuredBroker);
@ -563,7 +600,10 @@ namespace hass_workstation_service.Data
Username = broker?.Username, Username = broker?.Username,
Password = broker?.Password, Password = broker?.Password,
Port = broker?.Port, Port = broker?.Port,
UseTLS = broker?.UseTLS ?? false UseTLS = broker?.UseTLS ?? false,
RetainLWT = broker?.RetainLWT ?? true,
RootCAPath = broker?.RootCAPath,
ClientCertPath = broker?.RootCAPath
}; };
} }

@ -8,11 +8,33 @@ namespace hass_workstation_service.Data
private string username; private string username;
private string password; private string password;
private int? port; private int? port;
private string rootCAPath;
private string clientCertPath;
public string Host { get; set; } public string Host { get; set; }
public int Port { get => port ?? 1883; set => port = value; } public int Port { get => port ?? 1883; set => port = value; }
public bool UseTLS { get; set; } public bool UseTLS { get; set; }
// Before this option, Retains was the default, so let's keep that here to not break backwards compatibility
public bool RetainLWT { get; set; } = true;
public string RootCAPath {
get
{
if (rootCAPath!= null) return rootCAPath;
return "";
}
set => rootCAPath = value;
}
public string ClientCertPath {
get
{
if (clientCertPath != null) return clientCertPath;
return "";
}
set => clientCertPath = value;
}
public string Username public string Username
{ {

@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Commands
var message = new MqttApplicationMessageBuilder() var message = new MqttApplicationMessageBuilder()
.WithTopic(GetAutoDiscoveryConfig().State_topic) .WithTopic(GetAutoDiscoveryConfig().State_topic)
.WithPayload(state) .WithPayload(state)
.WithExactlyOnceQoS() //.WithExactlyOnceQoS()
.WithRetainFlag() //.WithRetainFlag()
.Build(); .Build();
await Publisher.Publish(message); await Publisher.Publish(message);
PreviousPublishedState = state; PreviousPublishedState = state;

@ -40,8 +40,8 @@ namespace hass_workstation_service.Domain.Sensors
var message = new MqttApplicationMessageBuilder() var message = new MqttApplicationMessageBuilder()
.WithTopic(GetAutoDiscoveryConfig().State_topic) .WithTopic(GetAutoDiscoveryConfig().State_topic)
.WithPayload(state) .WithPayload(state)
.WithExactlyOnceQoS() //.WithExactlyOnceQoS()
.WithRetainFlag() //.WithRetainFlag()
.Build(); .Build();
await Publisher.Publish(message); await Publisher.Publish(message);
PreviousPublishedState = state; PreviousPublishedState = state;

@ -43,7 +43,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content> </Content>
<Content Include="UserInterface.pdb"> <Content Include="UserInterface.pdb">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Never</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>

Loading…
Cancel
Save