implement custom command and availability topics wip

pull/34/head
sleevezipper 4 years ago
parent df8b564c37
commit 06b5373d6b

@ -124,3 +124,15 @@ This sensor returns the current session state. It has the following possible sta
### Dummy ### Dummy
This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it. This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.
## Commands
Commands can be used to trigger certain things on the client.
### CustomCommand
This command allows you to run any Windows Commands. The command will be run in a hidden Command Prompt. Some examples:
|Command|Explanation|
|---|---|
|Rundll32.exe user32.dll,LockWorkStation|This locks the current session.|

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,10 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.Util; using hass_workstation_service.Communication.Util;
using hass_workstation_service.Data; using hass_workstation_service.Data;
using hass_workstation_service.Domain.Commands;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using MQTTnet; using MQTTnet;
using MQTTnet.Adapter; using MQTTnet.Adapter;
@ -24,6 +27,7 @@ namespace hass_workstation_service.Communication
private string _mqttClientMessage { get; set; } private string _mqttClientMessage { get; set; }
public DateTime LastConfigAnnounce { get; private set; } public DateTime LastConfigAnnounce { get; private set; }
public DeviceConfigModel DeviceConfigModel { get; private set; } public DeviceConfigModel DeviceConfigModel { get; private set; }
public ICollection<AbstractCommand> Subscribers { get; private set; }
public bool IsConnected public bool IsConnected
{ {
get get
@ -44,7 +48,7 @@ namespace hass_workstation_service.Communication
DeviceConfigModel deviceConfigModel, DeviceConfigModel deviceConfigModel,
IConfigurationService configurationService) IConfigurationService configurationService)
{ {
this.Subscribers = new List<AbstractCommand>();
this._logger = logger; this._logger = logger;
this.DeviceConfigModel = deviceConfigModel; this.DeviceConfigModel = deviceConfigModel;
this._configurationService = configurationService; this._configurationService = configurationService;
@ -69,6 +73,8 @@ namespace hass_workstation_service.Communication
this._mqttClientMessage = "All good"; this._mqttClientMessage = "All good";
}); });
this._mqttClient.UseApplicationMessageReceivedHandler(e => this.HandleMessageReceived(e.ApplicationMessage));
// configure what happens on disconnect // configure what happens on disconnect
this._mqttClient.UseDisconnectedHandler(async e => this._mqttClient.UseDisconnectedHandler(async e =>
{ {
@ -103,7 +109,7 @@ namespace hass_workstation_service.Communication
} }
} }
public async Task AnnounceAutoDiscoveryConfig(AutoDiscoveryConfigModel config, bool clearConfig = false) public async Task AnnounceAutoDiscoveryConfig(DiscoveryConfigModel config, string domain, bool clearConfig = false)
{ {
if (this._mqttClient.IsConnected) if (this._mqttClient.IsConnected)
{ {
@ -111,11 +117,13 @@ namespace hass_workstation_service.Communication
{ {
PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(), PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(),
IgnoreNullValues = true, IgnoreNullValues = true,
PropertyNameCaseInsensitive = true PropertyNameCaseInsensitive = true,
}; };
var message = new MqttApplicationMessageBuilder() var message = new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/sensor/{this.DeviceConfigModel.Name}/{config.Name}/config") .WithTopic($"homeassistant/{domain}/{this.DeviceConfigModel.Name}/{config.Name}/config")
.WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, options)) .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, config.GetType(), options))
.WithRetainFlag() .WithRetainFlag()
.Build(); .Build();
await this.Publish(message); await this.Publish(message);
@ -147,5 +155,72 @@ namespace hass_workstation_service.Communication
{ {
return new MqqtClientStatus() { IsConnected = _mqttClient.IsConnected, Message = _mqttClientMessage }; return new MqqtClientStatus() { IsConnected = _mqttClient.IsConnected, Message = _mqttClientMessage };
} }
public async void AnnounceAvailability(string domain, bool offline = false)
{
if (this._mqttClient.IsConnected)
{
await this._mqttClient.PublishAsync(
new MqttApplicationMessageBuilder()
.WithTopic($"homeassistant/{domain}/{DeviceConfigModel.Name}/availability")
.WithPayload(offline ? "offline" : "online")
.Build()
);
}
else
{
this._logger.LogInformation($"Availability announce dropped because mqtt not connected");
}
}
public async Task DisconnectAsync()
{
if (this._mqttClient.IsConnected)
{
await this._mqttClient.DisconnectAsync();
}
else
{
this._logger.LogInformation($"Disconnected");
}
}
public async void Subscribe(AbstractCommand command)
{
if (this.IsConnected)
{
await this._mqttClient.SubscribeAsync(command.GetAutoDiscoveryConfig().Command_topic);
}
else
{
while (this.IsConnected == false)
{
await Task.Delay(5500);
}
await this._mqttClient.SubscribeAsync(command.GetAutoDiscoveryConfig().Command_topic);
}
Subscribers.Add(command);
}
private void HandleMessageReceived(MqttApplicationMessage applicationMessage)
{
foreach (AbstractCommand command in this.Subscribers)
{
if (command.GetAutoDiscoveryConfig().Command_topic == applicationMessage.Topic)
{
command.Execute();
}
}
Console.WriteLine("### RECEIVED APPLICATION MESSAGE ###");
Console.WriteLine($"+ Topic = {applicationMessage.Topic}");
Console.WriteLine($"+ Payload = {Encoding.UTF8.GetString(applicationMessage?.Payload)}");
Console.WriteLine($"+ QoS = {applicationMessage.QualityOfServiceLevel}");
Console.WriteLine($"+ Retain = {applicationMessage.Retain}");
Console.WriteLine();
}
} }
} }

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

@ -10,6 +10,7 @@ using System.Threading.Tasks;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.NamedPipe; using hass_workstation_service.Communication.NamedPipe;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Win32; using Microsoft.Win32;
@ -23,10 +24,12 @@ namespace hass_workstation_service.Data
public class ConfigurationService : IConfigurationService public class ConfigurationService : IConfigurationService
{ {
public ICollection<AbstractSensor> ConfiguredSensors { get; private set; } public ICollection<AbstractSensor> ConfiguredSensors { get; private set; }
public ICollection<AbstractCommand> ConfiguredCommands { get; private set; }
public Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; } public Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; }
private bool BrokerSettingsFileLocked { get; set; } private bool BrokerSettingsFileLocked { get; set; }
private bool SensorsSettingsFileLocked { get; set; } private bool SensorsSettingsFileLocked { get; set; }
private bool CommandSettingsFileLocked { get; set; }
private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service"); private readonly string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Hass Workstation Service");
@ -42,7 +45,13 @@ namespace hass_workstation_service.Data
File.Create(Path.Combine(path, "configured-sensors.json")).Close(); File.Create(Path.Combine(path, "configured-sensors.json")).Close();
} }
if (!File.Exists(Path.Combine(path, "configured-commands.json")))
{
File.Create(Path.Combine(path, "configured-commands.json")).Close();
}
ConfiguredSensors = new List<AbstractSensor>(); ConfiguredSensors = new List<AbstractSensor>();
ConfiguredCommands = new List<AbstractCommand>();
} }
public async void ReadSensorSettings(MqttPublisher publisher) public async void ReadSensorSettings(MqttPublisher publisher)
@ -123,6 +132,44 @@ namespace hass_workstation_service.Data
} }
} }
public async void ReadCommandSettings(MqttPublisher publisher)
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> commands = new List<ConfiguredCommand>();
using (var stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
{
Log.Logger.Information($"reading configured commands from: {stream.Name}");
if (stream.Length > 0)
{
commands = await JsonSerializer.DeserializeAsync<List<ConfiguredCommand>>(stream);
}
stream.Close();
this.CommandSettingsFileLocked = false;
}
foreach (ConfiguredCommand configuredCommand in commands)
{
AbstractCommand command = null;
switch (configuredCommand.Type)
{
case "CustomCommand":
command = new CustomCommand(publisher, configuredCommand.Command, configuredCommand.Name, configuredCommand.Id);
break;
default:
Log.Logger.Error("unsupported command type in config");
break;
}
if (command != null)
{
this.ConfiguredCommands.Add(command);
}
}
}
public async Task<IMqttClientOptions> GetMqttClientOptionsAsync() public async Task<IMqttClientOptions> GetMqttClientOptionsAsync()
{ {
ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync(); ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync();
@ -173,7 +220,7 @@ namespace hass_workstation_service.Data
return configuredBroker; return configuredBroker;
} }
public async void WriteSettingsAsync() public async void WriteSensorSettingsAsync()
{ {
while (this.SensorsSettingsFileLocked) while (this.SensorsSettingsFileLocked)
{ {
@ -210,11 +257,44 @@ namespace hass_workstation_service.Data
this.SensorsSettingsFileLocked = false; this.SensorsSettingsFileLocked = false;
} }
public async void WriteCommandSettingsAsync()
{
while (this.CommandSettingsFileLocked)
{
await Task.Delay(500);
}
this.CommandSettingsFileLocked = true;
List<ConfiguredCommand> configuredCommandsToSave = new List<ConfiguredCommand>();
using (FileStream stream = new FileStream(Path.Combine(path, "configured-commands.json"), FileMode.Open))
{
stream.SetLength(0);
Log.Logger.Information($"writing configured commands to: {stream.Name}");
foreach (AbstractCommand command in this.ConfiguredCommands)
{
if (command is CustomCommand customcommand)
{
configuredCommandsToSave.Add(new ConfiguredCommand() { Id = customcommand.Id, Name = customcommand.Name, Type = customcommand.GetType().Name, Command = customcommand.Command });
}
}
await JsonSerializer.SerializeAsync(stream, configuredCommandsToSave);
stream.Close();
}
this.CommandSettingsFileLocked = false;
}
public void AddConfiguredSensor(AbstractSensor sensor) public void AddConfiguredSensor(AbstractSensor sensor)
{ {
this.ConfiguredSensors.Add(sensor); this.ConfiguredSensors.Add(sensor);
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
WriteSettingsAsync(); WriteSensorSettingsAsync();
}
public void AddConfiguredCommand(AbstractCommand command)
{
this.ConfiguredCommands.Add(command);
command.PublishAutoDiscoveryConfigAsync();
WriteCommandSettingsAsync();
} }
public async void DeleteConfiguredSensor(Guid id) public async void DeleteConfiguredSensor(Guid id)
@ -224,7 +304,7 @@ namespace hass_workstation_service.Data
{ {
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync(); await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
this.ConfiguredSensors.Remove(sensorToRemove); this.ConfiguredSensors.Remove(sensorToRemove);
WriteSettingsAsync(); WriteSensorSettingsAsync();
} }
else else
{ {
@ -233,10 +313,26 @@ namespace hass_workstation_service.Data
} }
public async void DeleteConfiguredCommand(Guid id)
{
var sensorToRemove = this.ConfiguredCommands.FirstOrDefault(s => s.Id == id);
if (sensorToRemove != null)
{
await sensorToRemove.UnPublishAutoDiscoveryConfigAsync();
this.ConfiguredCommands.Remove(sensorToRemove);
WriteSensorSettingsAsync();
}
else
{
Log.Logger.Warning($"command with id {id} not found");
}
}
public void AddConfiguredSensors(List<AbstractSensor> sensors) public void AddConfiguredSensors(List<AbstractSensor> sensors)
{ {
sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor)); sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor));
WriteSettingsAsync(); WriteSensorSettingsAsync();
} }
/// <summary> /// <summary>

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

@ -1,5 +1,6 @@
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using MQTTnet.Client.Options; using MQTTnet.Client.Options;
using System; using System;
@ -13,16 +14,21 @@ namespace hass_workstation_service.Data
{ {
ICollection<AbstractSensor> ConfiguredSensors { get; } ICollection<AbstractSensor> ConfiguredSensors { get; }
Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; } Action<IMqttClientOptions> MqqtConfigChangedHandler { get; set; }
ICollection<AbstractCommand> ConfiguredCommands { get; }
void AddConfiguredCommand(AbstractCommand command);
void AddConfiguredSensor(AbstractSensor sensor); void AddConfiguredSensor(AbstractSensor sensor);
void AddConfiguredSensors(List<AbstractSensor> sensors); void AddConfiguredSensors(List<AbstractSensor> sensors);
Task<IMqttClientOptions> GetMqttClientOptionsAsync(); Task<IMqttClientOptions> GetMqttClientOptionsAsync();
void ReadSensorSettings(MqttPublisher publisher); void ReadSensorSettings(MqttPublisher publisher);
void WriteMqttBrokerSettingsAsync(MqttSettings settings); void WriteMqttBrokerSettingsAsync(MqttSettings settings);
void WriteSettingsAsync(); void WriteSensorSettingsAsync();
Task<MqttSettings> GetMqttBrokerSettings(); Task<MqttSettings> GetMqttBrokerSettings();
void EnableAutoStart(bool enable); void EnableAutoStart(bool enable);
bool IsAutoStartEnabled(); bool IsAutoStartEnabled();
void DeleteConfiguredSensor(Guid id); void DeleteConfiguredSensor(Guid id);
void DeleteConfiguredCommand(Guid id);
void WriteCommandSettingsAsync();
void ReadCommandSettings(MqttPublisher publisher);
} }
} }

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

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

@ -0,0 +1,48 @@
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 CustomCommand : AbstractCommand
{
public string Command { get; protected set; }
public CustomCommand(MqttPublisher publisher, string command, string name = "Custom", Guid id = default(Guid)) : base(publisher, name ?? "Custom", id)
{
this.Command = command;
}
public override void Execute()
{
System.Diagnostics.Process process = new System.Diagnostics.Process();
System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo();
startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden;
startInfo.CreateNoWindow = true;
startInfo.FileName = "cmd.exe";
startInfo.Arguments = $"/C {this.Command}";
process.StartInfo = startInfo;
process.Start();
}
public override CommandDiscoveryConfigModel GetAutoDiscoveryConfig()
{
return new CommandDiscoveryConfigModel()
{
Name = this.Name,
Unique_id = this.Id.ToString(),
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability",
Command_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/set",
Device = this.Publisher.DeviceConfigModel,
Expire_after = 60
};
}
public override string GetState()
{
return "off";
}
}
}

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

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

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

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

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

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

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

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

@ -21,15 +21,17 @@ namespace hass_workstation_service.Domain.Sensors
} }
else return "unsupported"; else return "unsupported";
} }
public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig()
{ {
return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel()
{ {
Name = this.Name, Name = this.Name,
Unique_id = this.Id.ToString(), Unique_id = this.Id.ToString(),
Device = this.Publisher.DeviceConfigModel, Device = this.Publisher.DeviceConfigModel,
State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state",
Icon = "mdi:microphone", Icon = "mdi:microphone",
Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability",
Expire_after = 60
}); });
} }

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

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

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

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

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

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using hass_workstation_service.Communication; using hass_workstation_service.Communication;
using hass_workstation_service.Data; using hass_workstation_service.Data;
using hass_workstation_service.Domain.Commands;
using hass_workstation_service.Domain.Sensors; using hass_workstation_service.Domain.Sensors;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -30,6 +31,7 @@ namespace hass_workstation_service
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
_configurationService.ReadCommandSettings(_mqttPublisher);
_configurationService.ReadSensorSettings(_mqttPublisher); _configurationService.ReadSensorSettings(_mqttPublisher);
while (!_mqttPublisher.IsConnected) while (!_mqttPublisher.IsConnected)
@ -40,11 +42,17 @@ namespace hass_workstation_service
_logger.LogInformation("Connected. Sending auto discovery messages."); _logger.LogInformation("Connected. Sending auto discovery messages.");
List<AbstractSensor> sensors = _configurationService.ConfiguredSensors.ToList(); List<AbstractSensor> sensors = _configurationService.ConfiguredSensors.ToList();
List<AbstractCommand> commands = _configurationService.ConfiguredCommands.ToList();
_mqttPublisher.AnnounceAvailability("sensor");
_mqttPublisher.AnnounceAvailability("switch");
foreach (AbstractSensor sensor in sensors) foreach (AbstractSensor sensor in sensors)
{ {
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
} }
foreach (AbstractCommand command in commands)
{
command.PublishAutoDiscoveryConfigAsync();
}
while (!stoppingToken.IsCancellationRequested) while (!stoppingToken.IsCancellationRequested)
{ {
sensors = _configurationService.ConfiguredSensors.ToList(); sensors = _configurationService.ConfiguredSensors.ToList();
@ -69,9 +77,24 @@ namespace hass_workstation_service
{ {
sensor.PublishAutoDiscoveryConfigAsync(); sensor.PublishAutoDiscoveryConfigAsync();
} }
foreach (AbstractCommand command in commands)
{
command.PublishAutoDiscoveryConfigAsync();
}
_mqttPublisher.AnnounceAvailability("sensor");
_mqttPublisher.AnnounceAvailability("switch");
} }
await Task.Delay(1000, stoppingToken); await Task.Delay(1000, stoppingToken);
} }
} }
public override async Task StopAsync(CancellationToken stoppingToken)
{
_mqttPublisher.AnnounceAvailability("sensor", true);
_mqttPublisher.AnnounceAvailability("switch", true);
await _mqttPublisher.DisconnectAsync();
}
} }
} }

Loading…
Cancel
Save