From 06b5373d6bbc0e14c4f469f3a178767a8326485a Mon Sep 17 00:00:00 2001 From: sleevezipper Date: Sat, 16 Jan 2021 04:12:12 +0100 Subject: [PATCH] implement custom command and availability topics wip --- README.md | 12 ++ UserInterface/UserInterface.csproj | 9 ++ .../ViewModels/AddCommandViewModel.cs | 31 ++++++ .../ViewModels/CommandSettingsViewModel.cs | 25 +++++ UserInterface/Views/AddCommandDialog.axaml | 26 +++++ UserInterface/Views/AddCommandDialog.axaml.cs | 97 ++++++++++++++++ UserInterface/Views/AppInfo.axaml | 17 +++ UserInterface/Views/AppInfo.axaml.cs | 91 +++++++++++++++ UserInterface/Views/CommandSettings.axaml | 26 +++++ UserInterface/Views/CommandSettings.axaml.cs | 82 ++++++++++++++ UserInterface/Views/MainWindow.axaml | 4 +- UserInterface/Views/SensorSettings.axaml | 1 + .../InterProcessApi.cs | 52 +++++++++ .../ServiceContractInterfaces.cs | 3 + .../ServiceContractModels.cs | 13 ++- .../Communication/MQTT/MqttPublisher.cs | 85 +++++++++++++- ...Model.cs => SensorDiscoveryConfigModel.cs} | 99 +++++++++++++++-- .../Data/ConfigurationService.cs | 104 +++++++++++++++++- .../Data/ConfiguredCommand.cs | 13 +++ .../Data/IConfigurationService.cs | 8 +- .../Domain/AbstractDiscoverable.cs | 13 +++ .../Domain/Commands/AbstractCommand.cs | 79 +++++++++++++ .../Domain/Commands/CustomCommand.cs | 48 ++++++++ .../Domain/Sensors/AbstractSensor.cs | 13 ++- .../Domain/Sensors/ActiveWindowSensor.cs | 8 +- .../Domain/Sensors/CPULoadSensor.cs | 10 +- .../Domain/Sensors/CurrentClockSpeedSensor.cs | 10 +- .../Domain/Sensors/DummySensor.cs | 8 +- .../Domain/Sensors/LastActiveSensor.cs | 10 +- .../Domain/Sensors/LastBootSensor.cs | 10 +- .../Domain/Sensors/MemoryUsageSensor.cs | 10 +- .../Domain/Sensors/MicrophoneActiveSensor.cs | 8 +- .../Domain/Sensors/NamedWindowSensor.cs | 8 +- .../Domain/Sensors/SessionStateSensor.cs | 8 +- .../Sensors/UserNotificationStateSensor.cs | 8 +- .../Domain/Sensors/WMIQuerySensor.cs | 8 +- .../Domain/Sensors/WebcamActiveSensor.cs | 8 +- hass-workstation-service/Worker.cs | 27 ++++- 38 files changed, 1018 insertions(+), 74 deletions(-) create mode 100644 UserInterface/ViewModels/AddCommandViewModel.cs create mode 100644 UserInterface/ViewModels/CommandSettingsViewModel.cs create mode 100644 UserInterface/Views/AddCommandDialog.axaml create mode 100644 UserInterface/Views/AddCommandDialog.axaml.cs create mode 100644 UserInterface/Views/AppInfo.axaml create mode 100644 UserInterface/Views/AppInfo.axaml.cs create mode 100644 UserInterface/Views/CommandSettings.axaml create mode 100644 UserInterface/Views/CommandSettings.axaml.cs rename hass-workstation-service/Communication/MQTT/{AutoDiscoveryConfigModel.cs => SensorDiscoveryConfigModel.cs} (62%) create mode 100644 hass-workstation-service/Data/ConfiguredCommand.cs create mode 100644 hass-workstation-service/Domain/AbstractDiscoverable.cs create mode 100644 hass-workstation-service/Domain/Commands/AbstractCommand.cs create mode 100644 hass-workstation-service/Domain/Commands/CustomCommand.cs diff --git a/README.md b/README.md index a508f42..cba50fc 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,15 @@ This sensor returns the current session state. It has the following possible sta ### Dummy 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.| \ No newline at end of file diff --git a/UserInterface/UserInterface.csproj b/UserInterface/UserInterface.csproj index c2877e3..7805ed5 100644 --- a/UserInterface/UserInterface.csproj +++ b/UserInterface/UserInterface.csproj @@ -23,9 +23,18 @@ + + AddCommandDialog.axaml + + + AppInfo.axaml + BackgroundServiceSettings.axaml + + CommandSettings.axaml + SensorSettings.axaml diff --git a/UserInterface/ViewModels/AddCommandViewModel.cs b/UserInterface/ViewModels/AddCommandViewModel.cs new file mode 100644 index 0000000..7fc458a --- /dev/null +++ b/UserInterface/ViewModels/AddCommandViewModel.cs @@ -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; } + } +} diff --git a/UserInterface/ViewModels/CommandSettingsViewModel.cs b/UserInterface/ViewModels/CommandSettingsViewModel.cs new file mode 100644 index 0000000..4477508 --- /dev/null +++ b/UserInterface/ViewModels/CommandSettingsViewModel.cs @@ -0,0 +1,25 @@ +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Text; + +namespace UserInterface.ViewModels +{ + public class CommandSettingsViewModel : ViewModelBase + { + private ICollection configuredCommands; + + public ICollection 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; } + } +} diff --git a/UserInterface/Views/AddCommandDialog.axaml b/UserInterface/Views/AddCommandDialog.axaml new file mode 100644 index 0000000..f359fc5 --- /dev/null +++ b/UserInterface/Views/AddCommandDialog.axaml @@ -0,0 +1,26 @@ + + + Sensor type + + + + + Name + + + + + + Command + + + + + diff --git a/UserInterface/Views/AddCommandDialog.axaml.cs b/UserInterface/Views/AddCommandDialog.axaml.cs new file mode 100644 index 0000000..9db4d7b --- /dev/null +++ b/UserInterface/Views/AddCommandDialog.axaml.cs @@ -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 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"); + this.comboBox.Items = Enum.GetValues(typeof(AvailableCommands)).Cast().OrderBy(v => v.ToString()); + this.comboBox.SelectedIndex = 0; + + // register IPC clients + ServiceProvider serviceProvider = new ServiceCollection() + .AddNamedPipeIpcClient("addCommand", pipeName: "pipeinternal") + .BuildServiceProvider(); + + // resolve IPC client factory + IIpcClientFactory clientFactory = serviceProvider + .GetRequiredService>(); + + // 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); + } + } +} diff --git a/UserInterface/Views/AppInfo.axaml b/UserInterface/Views/AppInfo.axaml new file mode 100644 index 0000000..5bbdfbc --- /dev/null +++ b/UserInterface/Views/AppInfo.axaml @@ -0,0 +1,17 @@ + + + + Info + + + + + + + + diff --git a/UserInterface/Views/AppInfo.axaml.cs b/UserInterface/Views/AppInfo.axaml.cs new file mode 100644 index 0000000..7a4c576 --- /dev/null +++ b/UserInterface/Views/AppInfo.axaml.cs @@ -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 client; + + public AppInfo() + { + this.InitializeComponent(); + // register IPC clients + ServiceProvider serviceProvider = new ServiceCollection() + .AddNamedPipeIpcClient("broker", pipeName: "pipeinternal") + .BuildServiceProvider(); + + // resolve IPC client factory + IIpcClientFactory clientFactory = serviceProvider + .GetRequiredService>(); + + // 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); + } + } +} diff --git a/UserInterface/Views/CommandSettings.axaml b/UserInterface/Views/CommandSettings.axaml new file mode 100644 index 0000000..587d109 --- /dev/null +++ b/UserInterface/Views/CommandSettings.axaml @@ -0,0 +1,26 @@ + + + Commands + + + + + + + + Add some commands by clicking the "Add" button. + + + + + diff --git a/UserInterface/Views/CommandSettings.axaml.cs b/UserInterface/Views/CommandSettings.axaml.cs new file mode 100644 index 0000000..fb963f4 --- /dev/null +++ b/UserInterface/Views/CommandSettings.axaml.cs @@ -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 client; + private DataGrid _dataGrid { get; set; } + private bool sensorsNeedToRefresh { get; set; } + + public CommandSettings() + { + this.InitializeComponent(); + // register IPC clients + ServiceProvider serviceProvider = new ServiceCollection() + .AddNamedPipeIpcClient("commands", pipeName: "pipeinternal") + .BuildServiceProvider(); + + // resolve IPC client factory + IIpcClientFactory clientFactory = serviceProvider + .GetRequiredService>(); + + // create client + this.client = clientFactory.CreateClient("commands"); + + + DataContext = new CommandSettingsViewModel(); + GetConfiguredCommands(); + + this._dataGrid = this.FindControl("Grid"); + } + + + public async void GetConfiguredCommands() + { + sensorsNeedToRefresh = false; + List 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); + } + + } +} diff --git a/UserInterface/Views/MainWindow.axaml b/UserInterface/Views/MainWindow.axaml index 50d8e41..c8f2cc3 100644 --- a/UserInterface/Views/MainWindow.axaml +++ b/UserInterface/Views/MainWindow.axaml @@ -14,9 +14,11 @@ - + + + diff --git a/UserInterface/Views/SensorSettings.axaml b/UserInterface/Views/SensorSettings.axaml index 48a2227..4d8d085 100644 --- a/UserInterface/Views/SensorSettings.axaml +++ b/UserInterface/Views/SensorSettings.axaml @@ -6,6 +6,7 @@ MaxWidth="800" x:Class="UserInterface.Views.SensorSettings" > + Sensors diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs index 4fe7dc5..da03e81 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs @@ -2,6 +2,7 @@ using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.NamedPipe; using hass_workstation_service.Communication.Util; using hass_workstation_service.Data; +using hass_workstation_service.Domain.Commands; using hass_workstation_service.Domain.Sensors; using Serilog; using System; @@ -49,11 +50,19 @@ namespace hass_workstation_service.Communication.InterProcesCommunication return "what?"; } + /// + /// This writes the provided settings to the config file. + /// + /// public void WriteMqttBrokerSettingsAsync(MqttSettings settings) { this._configurationService.WriteMqttBrokerSettingsAsync(settings); } + /// + /// Enables or disables autostart. + /// + /// public void EnableAutostart(bool 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(); } + public List 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) { this._configurationService.DeleteConfiguredSensor(id); } + /// + /// Adds a command to the configured commands. This properly initializes the class and writes it to the config file. + /// + /// + /// public void AddSensor(AvailableSensors sensorType, string json) { var serializerOptions = new JsonSerializerOptions @@ -133,5 +156,34 @@ namespace hass_workstation_service.Communication.InterProcesCommunication this._configurationService.AddConfiguredSensor(sensorToCreate); } } + + /// + /// Adds a command to the configured commands. This properly initializes the class, subscribes to the command topic and writes it to the config file. + /// + /// + /// + public void AddCommand(AvailableCommands commandType, string json) + { + var serializerOptions = new JsonSerializerOptions + { + Converters = { new DynamicJsonConverter() } + }; + dynamic model = JsonSerializer.Deserialize(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); + } + } } } diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs index a85f5fe..189bd5c 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs @@ -17,5 +17,8 @@ namespace hass_workstation_service.Communication.NamedPipe List GetConfiguredSensors(); void RemoveSensorById(Guid id); void AddSensor(AvailableSensors sensorType, string json); + void RemoveCommandById(Guid id); + List GetConfiguredCommands(); + void AddCommand(AvailableCommands commandType, string json); } } diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs index aee1b16..175ba8c 100644 --- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs +++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs @@ -29,7 +29,13 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models public int UpdateInterval { 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 { UserNotificationStateSensor, @@ -46,4 +52,9 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models LastBootSensor, SessionStateSensor } + + public enum AvailableCommands + { + CustomCommand + } } diff --git a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs index 090cb2e..37e32ae 100644 --- a/hass-workstation-service/Communication/MQTT/MqttPublisher.cs +++ b/hass-workstation-service/Communication/MQTT/MqttPublisher.cs @@ -1,10 +1,13 @@ using System; +using System.Collections.Generic; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.Util; using hass_workstation_service.Data; +using hass_workstation_service.Domain.Commands; using Microsoft.Extensions.Logging; using MQTTnet; using MQTTnet.Adapter; @@ -24,6 +27,7 @@ namespace hass_workstation_service.Communication private string _mqttClientMessage { get; set; } public DateTime LastConfigAnnounce { get; private set; } public DeviceConfigModel DeviceConfigModel { get; private set; } + public ICollection Subscribers { get; private set; } public bool IsConnected { get @@ -44,7 +48,7 @@ namespace hass_workstation_service.Communication DeviceConfigModel deviceConfigModel, IConfigurationService configurationService) { - + this.Subscribers = new List(); this._logger = logger; this.DeviceConfigModel = deviceConfigModel; this._configurationService = configurationService; @@ -69,6 +73,8 @@ namespace hass_workstation_service.Communication this._mqttClientMessage = "All good"; }); + this._mqttClient.UseApplicationMessageReceivedHandler(e => this.HandleMessageReceived(e.ApplicationMessage)); + // configure what happens on disconnect 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) { @@ -111,11 +117,13 @@ namespace hass_workstation_service.Communication { PropertyNamingPolicy = new CamelCaseJsonNamingpolicy(), IgnoreNullValues = true, - PropertyNameCaseInsensitive = true + PropertyNameCaseInsensitive = true, + }; + var message = new MqttApplicationMessageBuilder() - .WithTopic($"homeassistant/sensor/{this.DeviceConfigModel.Name}/{config.Name}/config") - .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, options)) + .WithTopic($"homeassistant/{domain}/{this.DeviceConfigModel.Name}/{config.Name}/config") + .WithPayload(clearConfig ? "" : JsonSerializer.Serialize(config, config.GetType(), options)) .WithRetainFlag() .Build(); await this.Publish(message); @@ -147,5 +155,72 @@ namespace hass_workstation_service.Communication { 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(); + } } } diff --git a/hass-workstation-service/Communication/MQTT/AutoDiscoveryConfigModel.cs b/hass-workstation-service/Communication/MQTT/SensorDiscoveryConfigModel.cs similarity index 62% rename from hass-workstation-service/Communication/MQTT/AutoDiscoveryConfigModel.cs rename to hass-workstation-service/Communication/MQTT/SensorDiscoveryConfigModel.cs index 4a9ae66..a67f6b6 100644 --- a/hass-workstation-service/Communication/MQTT/AutoDiscoveryConfigModel.cs +++ b/hass-workstation-service/Communication/MQTT/SensorDiscoveryConfigModel.cs @@ -3,18 +3,26 @@ using System.Collections.Generic; namespace hass_workstation_service.Communication { - public class AutoDiscoveryConfigModel + public abstract class DiscoveryConfigModel { /// - /// (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. /// /// - public string Availability_topic { get; set; } + public DeviceConfigModel Device { get; set; } /// - /// (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. /// /// - public DeviceConfigModel Device { get; set; } + public string Name { get; set; } + } + public class SensorDiscoveryConfigModel : DiscoveryConfigModel + { + /// + /// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates. + /// + /// + public string Availability_topic { get; set; } /// /// (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. /// @@ -47,11 +55,6 @@ namespace hass_workstation_service.Communication /// public string Json_attributes_topic { get; set; } /// - /// (Optional) The name of the MQTT sensor. Defaults to MQTT Sensor in hass. - /// - /// - public string Name { get; set; } - /// /// (Optional) The payload that represents the available state. /// /// @@ -89,6 +92,82 @@ namespace hass_workstation_service.Communication public string Value_template { get; set; } } + public class CommandDiscoveryConfigModel : DiscoveryConfigModel + { + /// + /// (Optional) The MQTT topic subscribed to receive availability (online/offline) updates. + /// + /// + public string Availability_topic { get; set; } + /// + /// (Optional) The MQTT topic to set the command + /// + /// + public string Command_topic { get; set; } + /// + /// (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. + /// + /// + public string Device_class { get; set; } + /// + /// (Optional) Defines the number of seconds after the sensor’s state expires, if it’s not updated. After expiry, the sensor’s state becomes unavailable. Defaults to 0 in hass. + /// + /// + public int? Expire_after { get; set; } + /// + /// Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. + /// + /// + public bool? Force_update { get; set; } + + /// + /// (Optional) The icon for the sensor. + /// + /// + public string Icon { get; set; } + /// + /// (Optional) Defines a template to extract the JSON dictionary from messages received on the json_attributes_topic. + /// + /// + public string Json_attributes_template { get; set; } + /// + /// (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. + /// + /// + public string Json_attributes_topic { get; set; } + /// + /// (Optional) The payload that represents the available state. + /// + /// + public string Payload_available { get; set; } + /// + /// (Optional) The payload that represents the unavailable state. + /// + /// + public string Payload_not_available { get; set; } + /// + /// (Optional) The maximum QoS level of the state topic. + /// + /// + public int? Qos { get; set; } + /// + /// The MQTT topic subscribed to receive sensor values. + /// + /// + public string State_topic { get; set; } + /// + /// (Optional) An ID that uniquely identifies this sensor. If two sensors have the same unique ID, Home Assistant will raise an exception. + /// + /// + public string Unique_id { get; set; } + /// + /// (Optional) Defines a template to extract the value. + /// + /// + public string Value_template { get; set; } + } + + /// /// This information will be used when announcing this device on the mqtt topic /// diff --git a/hass-workstation-service/Data/ConfigurationService.cs b/hass-workstation-service/Data/ConfigurationService.cs index 4527374..f130014 100644 --- a/hass-workstation-service/Data/ConfigurationService.cs +++ b/hass-workstation-service/Data/ConfigurationService.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using hass_workstation_service.Communication; using hass_workstation_service.Communication.InterProcesCommunication.Models; using hass_workstation_service.Communication.NamedPipe; +using hass_workstation_service.Domain.Commands; using hass_workstation_service.Domain.Sensors; using Microsoft.Extensions.Configuration; using Microsoft.Win32; @@ -23,10 +24,12 @@ namespace hass_workstation_service.Data public class ConfigurationService : IConfigurationService { public ICollection ConfiguredSensors { get; private set; } + public ICollection ConfiguredCommands { get; private set; } public Action MqqtConfigChangedHandler { get; set; } private bool BrokerSettingsFileLocked { 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"); @@ -42,7 +45,13 @@ namespace hass_workstation_service.Data 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(); + ConfiguredCommands = new List(); } 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 commands = new List(); + 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>(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 GetMqttClientOptionsAsync() { ConfiguredMqttBroker configuredBroker = await ReadMqttSettingsAsync(); @@ -173,7 +220,7 @@ namespace hass_workstation_service.Data return configuredBroker; } - public async void WriteSettingsAsync() + public async void WriteSensorSettingsAsync() { while (this.SensorsSettingsFileLocked) { @@ -210,11 +257,44 @@ namespace hass_workstation_service.Data this.SensorsSettingsFileLocked = false; } + public async void WriteCommandSettingsAsync() + { + while (this.CommandSettingsFileLocked) + { + await Task.Delay(500); + } + this.CommandSettingsFileLocked = true; + List configuredCommandsToSave = new List(); + 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) { this.ConfiguredSensors.Add(sensor); sensor.PublishAutoDiscoveryConfigAsync(); - WriteSettingsAsync(); + WriteSensorSettingsAsync(); + } + + public void AddConfiguredCommand(AbstractCommand command) + { + this.ConfiguredCommands.Add(command); + command.PublishAutoDiscoveryConfigAsync(); + WriteCommandSettingsAsync(); } public async void DeleteConfiguredSensor(Guid id) @@ -224,7 +304,7 @@ namespace hass_workstation_service.Data { await sensorToRemove.UnPublishAutoDiscoveryConfigAsync(); this.ConfiguredSensors.Remove(sensorToRemove); - WriteSettingsAsync(); + WriteSensorSettingsAsync(); } 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 sensors) { sensors.ForEach((sensor) => this.ConfiguredSensors.Add(sensor)); - WriteSettingsAsync(); + WriteSensorSettingsAsync(); } /// diff --git a/hass-workstation-service/Data/ConfiguredCommand.cs b/hass-workstation-service/Data/ConfiguredCommand.cs new file mode 100644 index 0000000..d5d6b48 --- /dev/null +++ b/hass-workstation-service/Data/ConfiguredCommand.cs @@ -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; } + } +} \ No newline at end of file diff --git a/hass-workstation-service/Data/IConfigurationService.cs b/hass-workstation-service/Data/IConfigurationService.cs index 3c63154..acbea17 100644 --- a/hass-workstation-service/Data/IConfigurationService.cs +++ b/hass-workstation-service/Data/IConfigurationService.cs @@ -1,5 +1,6 @@ using hass_workstation_service.Communication; using hass_workstation_service.Communication.InterProcesCommunication.Models; +using hass_workstation_service.Domain.Commands; using hass_workstation_service.Domain.Sensors; using MQTTnet.Client.Options; using System; @@ -13,16 +14,21 @@ namespace hass_workstation_service.Data { ICollection ConfiguredSensors { get; } Action MqqtConfigChangedHandler { get; set; } + ICollection ConfiguredCommands { get; } + void AddConfiguredCommand(AbstractCommand command); void AddConfiguredSensor(AbstractSensor sensor); void AddConfiguredSensors(List sensors); Task GetMqttClientOptionsAsync(); void ReadSensorSettings(MqttPublisher publisher); void WriteMqttBrokerSettingsAsync(MqttSettings settings); - void WriteSettingsAsync(); + void WriteSensorSettingsAsync(); Task GetMqttBrokerSettings(); void EnableAutoStart(bool enable); bool IsAutoStartEnabled(); void DeleteConfiguredSensor(Guid id); + void DeleteConfiguredCommand(Guid id); + void WriteCommandSettingsAsync(); + void ReadCommandSettings(MqttPublisher publisher); } } \ No newline at end of file diff --git a/hass-workstation-service/Domain/AbstractDiscoverable.cs b/hass-workstation-service/Domain/AbstractDiscoverable.cs new file mode 100644 index 0000000..7c388f7 --- /dev/null +++ b/hass-workstation-service/Domain/AbstractDiscoverable.cs @@ -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; } + } +} diff --git a/hass-workstation-service/Domain/Commands/AbstractCommand.cs b/hass-workstation-service/Domain/Commands/AbstractCommand.cs new file mode 100644 index 0000000..7e29b98 --- /dev/null +++ b/hass-workstation-service/Domain/Commands/AbstractCommand.cs @@ -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; } + /// + /// The update interval in seconds. It checks state only if the interval has passed. + /// + 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(); + } +} \ No newline at end of file diff --git a/hass-workstation-service/Domain/Commands/CustomCommand.cs b/hass-workstation-service/Domain/Commands/CustomCommand.cs new file mode 100644 index 0000000..aff15ec --- /dev/null +++ b/hass-workstation-service/Domain/Commands/CustomCommand.cs @@ -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"; + } + } +} diff --git a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs index 1b095bc..8c5cd50 100644 --- a/hass-workstation-service/Domain/Sensors/AbstractSensor.cs +++ b/hass-workstation-service/Domain/Sensors/AbstractSensor.cs @@ -6,7 +6,7 @@ using MQTTnet; namespace hass_workstation_service.Domain.Sensors { - public abstract class AbstractSensor + public abstract class AbstractSensor : AbstractDiscoverable { public Guid Id { 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 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) @@ -32,14 +33,14 @@ namespace hass_workstation_service.Domain.Sensors this.UpdateInterval = updateInterval; } - protected AutoDiscoveryConfigModel _autoDiscoveryConfigModel; - protected AutoDiscoveryConfigModel SetAutoDiscoveryConfigModel(AutoDiscoveryConfigModel config) + protected SensorDiscoveryConfigModel _autoDiscoveryConfigModel; + protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config) { this._autoDiscoveryConfigModel = config; return config; } - public abstract AutoDiscoveryConfigModel GetAutoDiscoveryConfig(); + public abstract SensorDiscoveryConfigModel GetAutoDiscoveryConfig(); public abstract string GetState(); public async Task PublishStateAsync() @@ -67,11 +68,11 @@ namespace hass_workstation_service.Domain.Sensors } public async void PublishAutoDiscoveryConfigAsync() { - await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig()); + await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain); } public async Task UnPublishAutoDiscoveryConfigAsync() { - await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), true); + await this.Publisher.AnnounceAutoDiscoveryConfig(this.GetAutoDiscoveryConfig(), this.Domain, true); } } diff --git a/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs b/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs index d431f14..79cdcb3 100644 --- a/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs +++ b/hass-workstation-service/Domain/Sensors/ActiveWindowSensor.cs @@ -9,15 +9,17 @@ namespace hass_workstation_service.Domain.Sensors 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 override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs b/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs index 7000943..21710fa 100644 --- a/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs +++ b/hass-workstation-service/Domain/Sensors/CPULoadSensor.cs @@ -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, Unique_id = this.Id.ToString(), 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", - Unit_of_measurement = "%" + Unit_of_measurement = "%", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs b/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs index 921a96d..5512d02 100644 --- a/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs +++ b/hass-workstation-service/Domain/Sensors/CurrentClockSpeedSensor.cs @@ -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 override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), 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", - Unit_of_measurement = "MHz" + Unit_of_measurement = "MHz", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } } diff --git a/hass-workstation-service/Domain/Sensors/DummySensor.cs b/hass-workstation-service/Domain/Sensors/DummySensor.cs index 5960104..b63cee1 100644 --- a/hass-workstation-service/Domain/Sensors/DummySensor.cs +++ b/hass-workstation-service/Domain/Sensors/DummySensor.cs @@ -13,14 +13,16 @@ namespace hass_workstation_service.Domain.Sensors 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, Unique_id = this.Id.ToString(), 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 }); } diff --git a/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs b/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs index 4a81342..07ef844 100644 --- a/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs +++ b/hass-workstation-service/Domain/Sensors/LastActiveSensor.cs @@ -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 override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", - Icon = "mdi:clock-time-three-outline" + State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:clock-time-three-outline", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/LastBootSensor.cs b/hass-workstation-service/Domain/Sensors/LastBootSensor.cs index b9bcbd6..bda0e89 100644 --- a/hass-workstation-service/Domain/Sensors/LastBootSensor.cs +++ b/hass-workstation-service/Domain/Sensors/LastBootSensor.cs @@ -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, Unique_id = this.Id.ToString(), Device = this.Publisher.DeviceConfigModel, - State_topic = $"homeassistant/sensor/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", - Icon = "mdi:clock-time-three-outline" + State_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/{this.Name}/state", + Icon = "mdi:clock-time-three-outline", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs b/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs index d905475..998369f 100644 --- a/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs +++ b/hass-workstation-service/Domain/Sensors/MemoryUsageSensor.cs @@ -34,16 +34,18 @@ namespace hass_workstation_service.Domain.Sensors } return ""; } - public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), 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", - Unit_of_measurement = "%" + Unit_of_measurement = "%", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } } diff --git a/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs index e85f854..7e78826 100644 --- a/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs +++ b/hass-workstation-service/Domain/Sensors/MicrophoneActiveSensor.cs @@ -21,15 +21,17 @@ namespace hass_workstation_service.Domain.Sensors } 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, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs index 7cf7989..a852bf0 100644 --- a/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs +++ b/hass-workstation-service/Domain/Sensors/NamedWindowSensor.cs @@ -16,15 +16,17 @@ namespace hass_workstation_service.Domain.Sensors 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, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs b/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs index dc20f38..02efbc5 100644 --- a/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs +++ b/hass-workstation-service/Domain/Sensors/SessionStateSensor.cs @@ -35,15 +35,17 @@ namespace hass_workstation_service.Domain.Sensors public class SessionStateSensor : AbstractSensor { public SessionStateSensor(MqttPublisher publisher, int? updateInterval = null, string name = "SessionState", Guid id = default(Guid)) : base(publisher, name ?? "SessionState", updateInterval ?? 10, id) { } - public override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs b/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs index e98e1d9..e566be6 100644 --- a/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs +++ b/hass-workstation-service/Domain/Sensors/UserNotificationStateSensor.cs @@ -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 override AutoDiscoveryConfigModel GetAutoDiscoveryConfig() + public override SensorDiscoveryConfigModel GetAutoDiscoveryConfig() { - return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new AutoDiscoveryConfigModel() + return this._autoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { Name = this.Name, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs index ed44784..1c2ff62 100644 --- a/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs +++ b/hass-workstation-service/Domain/Sensors/WMIQuerySensor.cs @@ -20,14 +20,16 @@ namespace hass_workstation_service.Domain.Sensors _objectQuery = new ObjectQuery(this.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, Unique_id = this.Id.ToString(), 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 }); } diff --git a/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs index c690c03..fc08ba3 100644 --- a/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs +++ b/hass-workstation-service/Domain/Sensors/WebcamActiveSensor.cs @@ -23,15 +23,17 @@ namespace hass_workstation_service.Domain.Sensors 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, Unique_id = this.Id.ToString(), 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", + Availability_topic = $"homeassistant/{this.Domain}/{Publisher.DeviceConfigModel.Name}/availability", + Expire_after = 60 }); } diff --git a/hass-workstation-service/Worker.cs b/hass-workstation-service/Worker.cs index 540225b..e91a9ca 100644 --- a/hass-workstation-service/Worker.cs +++ b/hass-workstation-service/Worker.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using hass_workstation_service.Communication; using hass_workstation_service.Data; +using hass_workstation_service.Domain.Commands; using hass_workstation_service.Domain.Sensors; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -30,6 +31,7 @@ namespace hass_workstation_service protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + _configurationService.ReadCommandSettings(_mqttPublisher); _configurationService.ReadSensorSettings(_mqttPublisher); while (!_mqttPublisher.IsConnected) @@ -40,11 +42,17 @@ namespace hass_workstation_service _logger.LogInformation("Connected. Sending auto discovery messages."); List sensors = _configurationService.ConfiguredSensors.ToList(); - + List commands = _configurationService.ConfiguredCommands.ToList(); + _mqttPublisher.AnnounceAvailability("sensor"); + _mqttPublisher.AnnounceAvailability("switch"); foreach (AbstractSensor sensor in sensors) { sensor.PublishAutoDiscoveryConfigAsync(); } + foreach (AbstractCommand command in commands) + { + command.PublishAutoDiscoveryConfigAsync(); + } while (!stoppingToken.IsCancellationRequested) { sensors = _configurationService.ConfiguredSensors.ToList(); @@ -60,7 +68,7 @@ namespace hass_workstation_service { Log.Logger.Warning("Sensor failed: " + sensor.Name, ex); } - + } // announce autodiscovery every 30 seconds if (_mqttPublisher.LastConfigAnnounce < DateTime.UtcNow.AddSeconds(-30)) @@ -69,9 +77,24 @@ namespace hass_workstation_service { sensor.PublishAutoDiscoveryConfigAsync(); } + foreach (AbstractCommand command in commands) + { + command.PublishAutoDiscoveryConfigAsync(); + } + _mqttPublisher.AnnounceAvailability("sensor"); + _mqttPublisher.AnnounceAvailability("switch"); } await Task.Delay(1000, stoppingToken); } + } + + public override async Task StopAsync(CancellationToken stoppingToken) + { + _mqttPublisher.AnnounceAvailability("sensor", true); + _mqttPublisher.AnnounceAvailability("switch", true); + await _mqttPublisher.DisconnectAsync(); + } + } }