diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index 7fb7314..9ec0268 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -7,6 +7,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; +using System.Windows.Threading; +using System.Runtime.ConstrainedExecution; +using System.Windows.Controls; +using System.Security.Authentication; +using System.Threading; namespace TEAMS2HA.API { @@ -21,6 +26,7 @@ public class MqttClientWrapper private const int RetryDelayMilliseconds = 2000; //wait a couple of seconds before retrying a connection attempt #endregion Private Fields + public event Action ConnectionStatusChanged; #region Public Constructors public bool IsAttemptingConnection @@ -28,12 +34,15 @@ public bool IsAttemptingConnection get { return _isAttemptingConnection; } private set { _isAttemptingConnection = value; } } - public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, string username, string password, bool useTls = false) + public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, string username, string password, bool UseTLS, bool IgnoreCertificateErrors) + { var factory = new MqttFactory(); _mqttClient = factory.CreateMqttClient() as MqttClient; - int mqttportInt = System.Convert.ToInt32(mqttPort); + int mqttportInt; + int.TryParse(mqttPort, out mqttportInt); + if (mqttportInt == 0) mqttportInt = 1883; var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() .WithClientId(clientId) @@ -41,38 +50,41 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st .WithCleanSession(); // If useTls is true or the port is 8883, configure the client to use TLS. - if (useTls || mqttportInt == 8883) + if (UseTLS || mqttportInt == 8883) { + var untrusted = IgnoreCertificateErrors; // Configure TLS options mqttClientOptionsBuilder.WithTcpServer(mqttBroker, mqttportInt) + .WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true, - AllowUntrustedCertificates = true, - IgnoreCertificateChainErrors = true, - IgnoreCertificateRevocationErrors = true + AllowUntrustedCertificates = untrusted, + IgnoreCertificateChainErrors = untrusted, + IgnoreCertificateRevocationErrors = untrusted }); + + Log.Information($"MQTT Client Created with TLS on port {mqttPort}."); + ConnectionStatusChanged?.Invoke($"MQTT Client Created with TLS"); + } else { mqttClientOptionsBuilder.WithTcpServer(mqttBroker, mqttportInt); Log.Information("MQTT Client Created with TCP."); + ConnectionStatusChanged?.Invoke($"MQTT Client Created with TCP"); } _mqttOptions = mqttClientOptionsBuilder.Build(); - _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + if (_mqttClient != null) + { + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + } + } - public MqttClientWrapper(/* parameters */) - { - // Existing initialization code... - - _mqttClient.ApplicationMessageReceivedAsync += HandleReceivedApplicationMessage; - Log.Information("MQTT Client Created"); - } - #endregion Public Constructors #region Public Events @@ -94,6 +106,7 @@ public async Task ConnectAsync() if (_mqttClient.IsConnected || _isAttemptingConnection) { Log.Information("MQTT client is already connected or connection attempt is in progress."); + return; } @@ -107,11 +120,15 @@ public async Task ConnectAsync() Log.Information($"Attempting to connect to MQTT (Attempt {retryCount + 1}/{MaxConnectionRetries})"); await _mqttClient.ConnectAsync(_mqttOptions); Log.Information("Connected to MQTT broker."); + if (_mqttClient.IsConnected) + ConnectionStatusChanged?.Invoke("MQTT Status: Connected"); + break; } catch (Exception ex) { Log.Debug($"Failed to connect to MQTT broker: {ex.Message}"); + ConnectionStatusChanged?.Invoke($"MQTT Status: Disconnected (Retry {retryCount + 1}) {ex.Message}"); retryCount++; await Task.Delay(RetryDelayMilliseconds); } @@ -120,6 +137,7 @@ public async Task ConnectAsync() _isAttemptingConnection = false; if (!_mqttClient.IsConnected) { + ConnectionStatusChanged?.Invoke("MQTT Status: Disconnected (Failed to connect)"); Log.Error("Failed to connect to MQTT broker after several attempts."); } } @@ -129,6 +147,7 @@ public async Task DisconnectAsync() if (!_mqttClient.IsConnected) { Log.Debug("MQTTClient is not connected"); + ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); return; } @@ -136,6 +155,7 @@ public async Task DisconnectAsync() { await _mqttClient.DisconnectAsync(); Log.Information("MQTT Disconnected"); + ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); } catch (Exception ex) { diff --git a/MainWindow.xaml b/MainWindow.xaml index 13ee56f..986562e 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -65,6 +65,8 @@ + + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index e974747..6266c56 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -95,6 +95,8 @@ public static AppSettings Instance public string MqttUsername { get; set; } public bool RunAtWindowsBoot { get; set; } + public bool UseTLS { get; set; } + public bool IgnoreCertificateErrors { get; set; } public bool RunMinimized { get; set; } @@ -173,6 +175,10 @@ private void LoadSettingsFromFile() { this.PlainTeamsToken = CryptoHelper.DecryptString(this.TeamsToken); } + if (string.IsNullOrEmpty(this.MqttPort)) + { + this.MqttPort = "1883"; // Default MQTT port + } } else { @@ -223,7 +229,7 @@ public MainWindow() { // Get the local application data folder path var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - + // Configure logging LoggingConfig.Configure(); @@ -261,7 +267,9 @@ public MainWindow() _settings.MqttAddress, _settings.MqttPort, _settings.MqttUsername, - _settings.MqttPassword + _settings.MqttPassword, + _settings.UseTLS, + _settings.IgnoreCertificateErrors ); // Set the action to be performed when a new token is updated @@ -289,6 +297,7 @@ public MainWindow() // Initialize the MQTT publish timer InitializeMqttPublishTimer(); + } #endregion Public Constructors @@ -309,8 +318,9 @@ public async Task InitializeMQTTConnection() if (mqttClientWrapper == null) { Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Client Not Initialized"); - return; Log.Debug("MQTT Client Not Initialized"); + return; + } //check we have at least an mqtt server address if (string.IsNullOrEmpty(_settings.MqttAddress)) @@ -321,18 +331,22 @@ public async Task InitializeMQTTConnection() } int retryCount = 0; const int maxRetries = 5; - + mqttClientWrapper.ConnectionStatusChanged += UpdateMqttConnectionStatus; while (retryCount < maxRetries && !mqttClientWrapper.IsConnected) { try { await mqttClientWrapper.ConnectAsync(); - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); + // Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - SetupMqttSensors(); - Log.Debug("MQTT Client Connected"); + mqttClientWrapper.MessageReceived += HandleIncomingCommand; - + if (mqttClientWrapper.IsConnected) + { + Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); + Log.Debug("MQTT Client Connected in InitializeMQTTConnection"); + SetupMqttSensors(); + } return; // Exit the method if connected } catch (Exception ex) @@ -390,26 +404,43 @@ protected override void OnStateChanged(EventArgs e) #endregion Protected Methods #region Private Methods - private async Task ReconnectToMqttServer() + private void UpdateMqttConnectionStatus(string status) { - // Disconnect from the current MQTT server + Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); + } + private void UpdateMqttClientWrapper() + { + + mqttClientWrapper = new MqttClientWrapper( + "TEAMS2HA", + _settings.MqttAddress, + _settings.MqttPort, + _settings.MqttUsername, + _settings.MqttPassword, + _settings.UseTLS, + _settings.IgnoreCertificateErrors + ); + + + // Subscribe to the ConnectionStatusChanged event + mqttClientWrapper.ConnectionStatusChanged += UpdateMqttConnectionStatus; + } + private async Task ReconnectToMqttServerAsync() + { + // Ensure disconnection from the current MQTT server, if connected if (mqttClientWrapper != null && mqttClientWrapper.IsConnected) { await mqttClientWrapper.DisconnectAsync(); } - // Create a new instance of MqttClientWrapper with new settings - mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword - ); + // Update the MQTT client wrapper with new settings + UpdateMqttClientWrapper(); - // Connect to the new MQTT server + // Attempt to connect to the MQTT server with new settings await mqttClientWrapper.ConnectAsync(); } + + private void SetWindowTitle() { var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; @@ -660,7 +691,7 @@ private async Task HandleIncomingCommand(MqttApplicationMessageReceivedEventArgs // Check if it's a command topic and handle accordingly if (topic.StartsWith("homeassistant/switch/") && topic.EndsWith("/set")) { - string command = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + string command = Encoding.UTF8.GetString(e.ApplicationMessage.PayloadSegment); // Parse and handle the command HandleSwitchCommand(topic, command); } @@ -789,6 +820,8 @@ private async void MainPage_Loaded(object sender, RoutedEventArgs e) RunAtWindowsBootCheckBox.IsChecked = _settings.RunAtWindowsBoot; RunMinimisedCheckBox.IsChecked = _settings.RunMinimized; MqttUserNameBox.Text = _settings.MqttUsername; + UseTLS.IsChecked = _settings.UseTLS; + IgnoreCert.IsChecked = _settings.IgnoreCertificateErrors; MQTTPasswordBox.Password = _settings.MqttPassword; MqttAddress.Text = _settings.MqttAddress; MqttPort.Text = _settings.MqttPort; @@ -914,54 +947,38 @@ private async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSetting } - - private bool SaveSettings() + + private async Task SaveSettingsAsync() { var settings = AppSettings.Instance; - bool mqttSettingsChanged = - settings.MqttAddress != MqttAddress.Text || - settings.MqttUsername != MqttUserNameBox.Text || - settings.MqttPort != MqttPort.Text || - settings.MqttPassword != MQTTPasswordBox.Password; + // Gather the current settings from UI components (make sure to do this on the UI thread) + Dispatcher.Invoke(() => + { + settings.MqttAddress = MqttAddress.Text; + settings.MqttPort = MqttPort.Text; + settings.MqttUsername = MqttUserNameBox.Text; + settings.MqttPassword = MQTTPasswordBox.Password; + settings.UseTLS = UseTLS.IsChecked ?? false; + settings.IgnoreCertificateErrors = IgnoreCert.IsChecked ?? false; + // Additional settings as needed + }); - settings.RunAtWindowsBoot = RunAtWindowsBootCheckBox.IsChecked ?? false; - settings.RunMinimized = RunMinimisedCheckBox.IsChecked ?? false; - settings.MqttAddress = MqttAddress.Text; - settings.MqttPort = MqttPort.Text; - settings.MqttUsername = MqttUserNameBox.Text; - settings.MqttPassword = MQTTPasswordBox.Password; - settings.Theme = isDarkTheme ? "Dark" : "Light"; + // Check if MQTT settings have changed (consider abstracting this logic into a separate method) + bool mqttSettingsChanged = CheckIfMqttSettingsChanged(settings); // Save the updated settings to file settings.SaveSettingsToFile(); - if (mqttSettingsChanged) - { - // Run the reconnection on a background thread to avoid UI freeze - Task.Run(async () => await ReconnectToMqttServer()).Wait(); - } - return mqttSettingsChanged; + + await ReconnectToMqttServerAsync(); + } - private void SaveSettings_Click(object sender, RoutedEventArgs e) + private async void SaveSettings_Click(object sender, RoutedEventArgs e) { Log.Debug("SaveSettings_Click: Save Settings Clicked" + _settings.ToString); - bool mqttSettingsChanged = SaveSettings(); - if (mqttSettingsChanged) - { - // Retry MQTT connection with new settings - mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword - ); - _ = InitializeMQTTConnection(); - Log.Debug("SaveSettings_Click: MQTT Settings Changed and initialze called"); - //if mqtt is connected, disable the test mqtt connection button - } + await SaveSettingsAsync(); } private async Task SetStartupAsync(bool startWithWindows) @@ -1031,7 +1048,7 @@ private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); UpdateStatusMenuItems(); - Log.Debug("TestMQTTConnection_Click: MQTT Client Connected"); + Log.Debug("TestMQTTConnection_Click: MQTT Client Connected in testmqttconnection"); return; } //make sure we have an mqtt address @@ -1051,9 +1068,12 @@ private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) try { await mqttClientWrapper.ConnectAsync(); - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); + if (mqttClientWrapper.IsConnected) + { + Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); + } UpdateStatusMenuItems(); - Log.Debug("TestMQTTConnection_Click: MQTT Client Connected"); + Log.Debug("TestMQTTConnection_Click: MQTT Client Connected in TestMQTTConnection_Click"); return; // Exit the method if connected } catch (Exception ex) @@ -1089,6 +1109,16 @@ private async void TestTeamsConnection_Click(object sender, RoutedEventArgs e) await _teamsClient.PairWithTeamsAsync(); } } + private bool CheckIfMqttSettingsChanged(AppSettings newSettings) + { + var currentSettings = AppSettings.Instance; + return newSettings.MqttAddress != currentSettings.MqttAddress || + newSettings.MqttPort != currentSettings.MqttPort || + newSettings.MqttUsername != currentSettings.MqttUsername || + newSettings.MqttPassword != currentSettings.MqttPassword || + newSettings.UseTLS != currentSettings.UseTLS || + newSettings.IgnoreCertificateErrors != currentSettings.IgnoreCertificateErrors; + } private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) { @@ -1098,7 +1128,7 @@ private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) ApplyTheme(_settings.Theme); // Save settings after changing the theme - SaveSettings(); + _ = SaveSettingsAsync(); } #endregion Private Methods diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 37d73da..abd0e85 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.248 - 1.1.0.248 + 1.1.0.276 + 1.1.0.276 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png