From 3954e75d741e43a773b9150e8d5daddbd82906e6 Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 10 Mar 2024 13:07:36 +0000 Subject: [PATCH 01/31] add teams running sensor --- API/State.cs | 1 + MainWindow.xaml.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/API/State.cs b/API/State.cs index cc6fbf4..cda2206 100644 --- a/API/State.cs +++ b/API/State.cs @@ -138,6 +138,7 @@ public string Microphone } } } + public bool teamsRunning { get; set; } public string Recording { diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 42c37e6..14df2f1 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -216,7 +216,7 @@ public partial class MainWindow : Window private List sensorNames = new List { - "IsMuted", "IsVideoOn", "IsHandRaised", "IsInMeeting", "IsRecordingOn", "IsBackgroundBlurred", "IsSharing", "HasUnreadMessages" + "IsMuted", "IsVideoOn", "IsHandRaised", "IsInMeeting", "IsRecordingOn", "IsBackgroundBlurred", "IsSharing", "HasUnreadMessages", "teamsRunning" }; private bool teamspaired = false; @@ -628,6 +628,7 @@ private string DetermineDeviceClass(string sensor) case "HasUnreadMessages": case "IsRecordingOn": case "IsSharing": + case "teamsRunning": return "sensor"; // These are true/false sensors default: return null; // Or a default device class if appropriate @@ -786,6 +787,20 @@ private async Task initializeteamsconnection() { Log.Debug("initializeteamsconnection: WebSocketClient is already connected or in the process of connecting"); } + if (_teamsClient.IsConnected) + { + Dispatcher.Invoke(() => TeamsConnectionStatus.Text = "Teams Status: Connected"); + // ADD in code to set the connected status as a sensor + status + State.Instance.teamsRunning = true; + Log.Debug("initializeteamsconnection: WebSocketClient Connected"); + } + else + { + Dispatcher.Invoke(() => TeamsConnectionStatus.Text = "Teams Status: Disconnected"); + State.Instance.teamsRunning = false; + Log.Debug("initializeteamsconnection: WebSocketClient Disconnected"); + } } private void LogsButton_Click(object sender, RoutedEventArgs e) From f3c1f2f4b03b92f1f712fb75a9faca5ba3255252 Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 10 Mar 2024 13:10:08 +0000 Subject: [PATCH 02/31] . --- MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 14df2f1..8375126 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -791,7 +791,7 @@ private async Task initializeteamsconnection() { Dispatcher.Invoke(() => TeamsConnectionStatus.Text = "Teams Status: Connected"); // ADD in code to set the connected status as a sensor - status + State.Instance.teamsRunning = true; Log.Debug("initializeteamsconnection: WebSocketClient Connected"); } From 7e0ea16d289dcff3fe5071159dd0d6d6b82d697e Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 10 Mar 2024 13:20:54 +0000 Subject: [PATCH 03/31] add sensor for teams running --- API/MqttClientWrapper.cs | 3 ++- API/State.cs | 2 +- MainWindow.xaml.cs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index 2702119..5d0b195 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -227,7 +227,8 @@ public static List GetEntityNames(string deviceId) $"sensor.{deviceId}_isinmeeting", $"sensor.{deviceId}_issharing", $"sensor.{deviceId}_hasunreadmessages", - $"switch.{deviceId}_isbackgroundblurred" + $"switch.{deviceId}_isbackgroundblurred", + $"sensor.{deviceId}_teamsRunning" }; diff --git a/API/State.cs b/API/State.cs index cda2206..bae0c5c 100644 --- a/API/State.cs +++ b/API/State.cs @@ -32,7 +32,7 @@ public class State // Define properties for the different components of the state private string _status = ""; - + private bool _teamsRunning; #endregion Private Fields #region Public Delegates diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 8375126..c4322c6 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -674,6 +674,7 @@ private string DetermineIcon(string sensor, MeetingState state) // If the sensor does not match any of the above cases, return "mdi:eye" _ => "mdi:eye" + }; } From 29753112a4c6762857e802026ead88888c9f7ab6 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 14:33:57 +0000 Subject: [PATCH 04/31] menu --- MainWindow.xaml.cs | 18 ++++++++++++------ TEAMS2HA.csproj | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 42c37e6..8acbb37 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -417,6 +417,7 @@ protected override void OnStateChanged(EventArgs e) private void UpdateMqttConnectionStatus(string status) { Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } private void UpdateMqttClientWrapper() { @@ -446,7 +447,7 @@ private async Task ReconnectToMqttServerAsync() // Update the MQTT client wrapper with new settings UpdateMqttClientWrapper(); - + Dispatcher.Invoke(() => UpdateStatusMenuItems()); // Attempt to connect to the MQTT server with new settings await mqttClientWrapper.ConnectAsync(); //we need to subscribe again (Thanks to @egglestron for pointing this out!) @@ -477,10 +478,12 @@ private async void ReestablishConnections() await mqttClientWrapper.ConnectAsync(); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); await SetupMqttSensors(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } if (!_teamsClient.IsConnected) { await initializeteamsconnection(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } } catch (Exception ex) @@ -602,6 +605,7 @@ private async void CheckMqttConnection() await mqttClientWrapper.ConnectAsync(); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); UpdateConnectionStatus(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } } @@ -610,7 +614,7 @@ private void UpdateConnectionStatus() Dispatcher.Invoke(() => { MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - UpdateStatusMenuItems(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); }); } @@ -869,7 +873,7 @@ private async void MainPage_Loaded(object sender, RoutedEventArgs e) this.Hide(); MyNotifyIcon.Visibility = Visibility.Visible; // Show the NotifyIcon in the system tray } - UpdateStatusMenuItems(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } private void MainPage_Unloaded(object sender, RoutedEventArgs e) @@ -903,6 +907,7 @@ private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) string keepAliveMessage = "alive"; _ = mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } } @@ -1093,6 +1098,7 @@ private void TeamsConnectionStatusChanged(bool isConnected) { TeamsConnectionStatus.Text = isConnected ? "Teams: Connected" : "Teams: Disconnected"; UpdateStatusMenuItems(); + Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); }); } @@ -1136,7 +1142,7 @@ private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); } - UpdateStatusMenuItems(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); Log.Debug("TestMQTTConnection_Click: MQTT Client Connected in TestMQTTConnection_Click"); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); return; // Exit the method if connected @@ -1145,7 +1151,7 @@ private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) { Dispatcher.Invoke(() => MQTTConnectionStatus.Text = $"MQTT Status: Disconnected (Retry {retryCount + 1})"); Log.Debug("TestMQTTConnection_Click: MQTT Client Failed to Connect {message}", ex.Message); - UpdateStatusMenuItems(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); retryCount++; await Task.Delay(2000); // Wait for 2 seconds before retrying } @@ -1153,7 +1159,7 @@ private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Disconnected (Failed to connect)"); Log.Debug("TestMQTTConnection_Click: MQTT Client Failed to Connect"); - UpdateStatusMenuItems(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } private async void TestTeamsConnection_Click(object sender, RoutedEventArgs e) diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 7997435..dd801ed 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.340 - 1.1.0.340 + 1.1.0.346 + 1.1.0.346 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 3e2ff576e3d002810edf7c6c650e626864ba7564 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 15:19:49 +0000 Subject: [PATCH 05/31] Made sensors binary sensors --- API/State.cs | 2 +- MainWindow.xaml.cs | 44 ++++++++++++++++++++++++++++++++++++++------ TEAMS2HA.csproj | 4 ++-- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/API/State.cs b/API/State.cs index bae0c5c..a2e5a98 100644 --- a/API/State.cs +++ b/API/State.cs @@ -32,7 +32,7 @@ public class State // Define properties for the different components of the state private string _status = ""; - private bool _teamsRunning; + private bool _teamsRunning = false; #endregion Private Fields #region Public Delegates diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index edf04fc..55b8931 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -687,15 +687,21 @@ private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) switch (sensor) { case "IsMuted": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; case "IsVideoOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; case "IsBackgroundBlurred": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; case "IsHandRaised": // Cast to bool and then check the value return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; case "IsInMeeting": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; case "HasUnreadMessages": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; case "IsRecordingOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; case "IsSharing": // Similar casting for these properties return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; @@ -993,17 +999,25 @@ private async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSetting } else if (deviceClass == "sensor") { - var sensorConfig = new + var binarySensorConfig = new { name = sensorName, unique_id = uniqueId, device = deviceInfo, icon = icon, - state_topic = $"homeassistant/sensor/{sensorName}/state" + state_topic = $"homeassistant/binary_sensor/{sensorName}/state", + payload_on = "true", // Assuming "True" states map to "ON" + payload_off = "false" // Assuming "False" states map to "OFF" }; - string configTopic = $"homeassistant/sensor/{sensorName}/config"; - await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(sensorConfig), true); - await mqttClientWrapper.PublishAsync(sensorConfig.state_topic, stateValue); + //string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; + //await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); + //await mqttClientWrapper.PublishAsync(binarySensorConfig.state_topic, stateValue); + string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; + await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); + + // Here's the important part: publish the initial state for each sensor + string stateTopic = $"homeassistant/binary_sensor/{sensorName}/state"; + await mqttClientWrapper.PublishAsync(stateTopic, stateValue.ToLowerInvariant()); // Convert "True"/"False" to "on"/"off" or keep "ON"/"OFF" } } } @@ -1114,10 +1128,28 @@ private void TeamsConnectionStatusChanged(bool isConnected) { TeamsConnectionStatus.Text = isConnected ? "Teams: Connected" : "Teams: Disconnected"; UpdateStatusMenuItems(); - + State.Instance.teamsRunning = isConnected; Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); + PublishTeamsConnectionStatus(isConnected); }); } + private async void PublishTeamsConnectionStatus(bool isConnected) + { + // Construct the topic and payload according to your naming convention + string topic = $"homeassistant/binary_sensor/{deviceid}_teamsConnected/state"; + string payload = isConnected ? "ON" : "OFF"; // Use "ON" or "OFF" for binary_sensor in Home Assistant + + // Use your existing MQTT client wrapper to publish the status + if (mqttClientWrapper != null && mqttClientWrapper.IsConnected) + { + await mqttClientWrapper.PublishAsync(topic, payload); + Log.Debug("Published Teams connection status: {status}", payload); + } + else + { + Log.Error("Cannot publish Teams connection status: MQTT client is not connected."); + } + } private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) { diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index dd801ed..40b2002 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.346 - 1.1.0.346 + 1.1.0.355 + 1.1.0.355 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From baa51572fa0426de928d840f2fb5f966612aed0a Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 16:02:31 +0000 Subject: [PATCH 06/31] Binary Sensors Working, Teams Running Sensor working --- API/MeetingState.cs | 2 +- API/Teams.cs | 10 ++++- MainWindow.xaml.cs | 99 +++++++++------------------------------------ TEAMS2HA.csproj | 4 +- 4 files changed, 32 insertions(+), 83 deletions(-) diff --git a/API/MeetingState.cs b/API/MeetingState.cs index 9372384..35cb215 100644 --- a/API/MeetingState.cs +++ b/API/MeetingState.cs @@ -36,7 +36,7 @@ public class MeetingState public bool IsBackgroundBlurred { get; set; } public bool IsSharing { get; set; } public bool HasUnreadMessages { get; set; } - + public bool teamsRunning { get; set; } #endregion Public Properties } diff --git a/API/Teams.cs b/API/Teams.cs index c41a787..5b4c860 100644 --- a/API/Teams.cs +++ b/API/Teams.cs @@ -237,6 +237,7 @@ private void OnMessageReceived(object sender, string message) meetingState["isRecordingOn"] = meetingUpdate.MeetingState.IsRecordingOn; meetingState["isBackgroundBlurred"] = meetingUpdate.MeetingState.IsBackgroundBlurred; meetingState["isSharing"] = meetingUpdate.MeetingState.IsSharing; + meetingUpdate.MeetingState.teamsRunning = IsConnected; if (meetingUpdate.MeetingState.IsVideoOn) { State.Instance.Camera = "On"; @@ -245,7 +246,14 @@ private void OnMessageReceived(object sender, string message) { State.Instance.Camera = "Off"; } - + if (meetingUpdate.MeetingState.teamsRunning) + { + State.Instance.teamsRunning = true; + } + else + { + State.Instance.teamsRunning = false; + } if (meetingUpdate.MeetingState.IsInMeeting) { State.Instance.Activity = "In a meeting"; diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 55b8931..a62a5a6 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -705,7 +705,8 @@ private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) case "IsSharing": // Similar casting for these properties return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - + case "teamsRunning": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; default: return "unknown"; } @@ -963,7 +964,9 @@ private async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSetting IsRecordingOn = false, IsBackgroundBlurred = false, IsSharing = false, - HasUnreadMessages = false + HasUnreadMessages = false, + teamsRunning = false + } }; } @@ -1099,7 +1102,8 @@ private async Task SetupMqttSensors() IsRecordingOn = false, IsBackgroundBlurred = false, IsSharing = false, - HasUnreadMessages = false + HasUnreadMessages = false, + teamsRunning = false } }; @@ -1128,87 +1132,24 @@ private void TeamsConnectionStatusChanged(bool isConnected) { TeamsConnectionStatus.Text = isConnected ? "Teams: Connected" : "Teams: Disconnected"; UpdateStatusMenuItems(); - State.Instance.teamsRunning = isConnected; - Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); - PublishTeamsConnectionStatus(isConnected); - }); - } - private async void PublishTeamsConnectionStatus(bool isConnected) - { - // Construct the topic and payload according to your naming convention - string topic = $"homeassistant/binary_sensor/{deviceid}_teamsConnected/state"; - string payload = isConnected ? "ON" : "OFF"; // Use "ON" or "OFF" for binary_sensor in Home Assistant - - // Use your existing MQTT client wrapper to publish the status - if (mqttClientWrapper != null && mqttClientWrapper.IsConnected) - { - await mqttClientWrapper.PublishAsync(topic, payload); - Log.Debug("Published Teams connection status: {status}", payload); - } - else - { - Log.Error("Cannot publish Teams connection status: MQTT client is not connected."); - } - } - - private async void TestMQTTConnection_Click(object sender, RoutedEventArgs e) - { - Log.Debug("Testing MQTT COnnection"); - if (mqttClientWrapper == null) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Client Not Initialized"); - UpdateStatusMenuItems(); - Log.Debug("TestMQTTConnection_Click: MQTT Client Not Initialized"); - return; - } - //we need to test to see if we are already connected - if (mqttClientWrapper.IsConnected) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); - UpdateStatusMenuItems(); - Log.Debug("TestMQTTConnection_Click: MQTT Client Connected in testmqttconnection"); - return; - } - //make sure we have an mqtt address - if (string.IsNullOrEmpty(_settings.MqttAddress)) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Server Address Not Set"); - UpdateStatusMenuItems(); - Log.Debug("TestMQTTConnection_Click: MQTT Server Address Not Set"); - return; - } - //we are not connected so lets try to connect - int retryCount = 0; - const int maxRetries = 5; - - while (retryCount < maxRetries && !mqttClientWrapper.IsConnected) - { - try + if(isConnected == true) { - await mqttClientWrapper.ConnectAsync(); - if (mqttClientWrapper.IsConnected) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); - } - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - Log.Debug("TestMQTTConnection_Click: MQTT Client Connected in TestMQTTConnection_Click"); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - return; // Exit the method if connected + State.Instance.teamsRunning = true; + } - catch (Exception ex) + else { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = $"MQTT Status: Disconnected (Retry {retryCount + 1})"); - Log.Debug("TestMQTTConnection_Click: MQTT Client Failed to Connect {message}", ex.Message); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - retryCount++; - await Task.Delay(2000); // Wait for 2 seconds before retrying + State.Instance.teamsRunning = false; + _= PublishConfigurations(null, _settings); } - } - - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Disconnected (Failed to connect)"); - Log.Debug("TestMQTTConnection_Click: MQTT Client Failed to Connect"); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); + + Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); + + }); } + + + private async void TestTeamsConnection_Click(object sender, RoutedEventArgs e) { diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 40b2002..7387e3d 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.355 - 1.1.0.355 + 1.1.0.373 + 1.1.0.373 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From b72962056d7f60d424a2f9cf13bc6afeac02c730 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 17:08:38 +0000 Subject: [PATCH 07/31] Refactor MQTT code into new class --- API/MqttManager.cs | 445 +++++++++++++++++++++++++++++++++++++++++++++ MainWindow.xaml.cs | 387 +++++---------------------------------- TEAMS2HA.csproj | 4 +- 3 files changed, 491 insertions(+), 345 deletions(-) create mode 100644 API/MqttManager.cs diff --git a/API/MqttManager.cs b/API/MqttManager.cs new file mode 100644 index 0000000..6cc1ff5 --- /dev/null +++ b/API/MqttManager.cs @@ -0,0 +1,445 @@ +using MQTTnet.Client; +using MQTTnet.Protocol; +using System; +using System.Collections.Generic; +using System.Linq; +using Serilog; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Threading; +using Newtonsoft.Json; + +namespace TEAMS2HA.API +{ + public class MqttManager + { + #region Private Fields + + private readonly string _deviceId; + private readonly List _sensorNames; + private readonly AppSettings _settings; + private MqttClientWrapper _mqttClientWrapper; + private Dictionary _previousSensorStates; + public event Action StatusUpdated; + public delegate Task CommandToTeamsHandler(string jsonMessage); + public event CommandToTeamsHandler CommandToTeams; + #endregion Private Fields + + #region Public Constructors + + public MqttManager(MqttClientWrapper mqttClientWrapper, AppSettings settings, List sensorNames, string deviceId) + { + _mqttClientWrapper = mqttClientWrapper; + _settings = settings; + _sensorNames = sensorNames; + _deviceId = deviceId; + _previousSensorStates = new Dictionary(); + InitializeConnection(); + } + + #endregion Public Constructors + + #region Public Delegates + + public delegate void ConnectionStatusChangedHandler(string status); + + #endregion Public Delegates + + #region Public Events + + public event ConnectionStatusChangedHandler ConnectionStatusChanged; + + #endregion Public Events + + #region Public Methods + + public async Task HandleIncomingCommand(MqttApplicationMessageReceivedEventArgs e) + { + string topic = e.ApplicationMessage.Topic; + Log.Debug("HandleIncomingCommand: MQTT Topic {topic}", topic); + // 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.PayloadSegment); + // Parse and handle the command + HandleSwitchCommand(topic, command); + } + } + private async void HandleSwitchCommand(string topic, string command) + { + // Determine which switch is being controlled based on the topic + string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" + int underscoreIndex = switchName.IndexOf('_'); + if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) + { + switchName = switchName.Substring(underscoreIndex + 1); + } + string jsonMessage = ""; + switch (switchName) + { + case "ismuted": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isvideoon": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isbackgroundblurred": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "ishandraised": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + // Add other cases as needed + } + + if (!string.IsNullOrEmpty(jsonMessage)) + { + // Raise the event + CommandToTeams?.Invoke(jsonMessage); + } + } + public async Task InitializeConnection() + { + if (_mqttClientWrapper == null) + { + UpdateConnectionStatus("MQTT Client Not Initialized"); + Log.Debug("MQTT Client Not Initialized"); + return; + } + //check we have at least an mqtt server address + if (string.IsNullOrEmpty(_settings.MqttAddress)) + { + UpdateConnectionStatus("MQTT Server Address Not Set"); + Log.Debug("MQTT Server Address Not Set"); + return; + } + int retryCount = 0; + const int maxRetries = 5; + + while (retryCount < maxRetries && !_mqttClientWrapper.IsConnected) + { + try + { + await _mqttClientWrapper.ConnectAsync(); + // Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); + await _mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + + _mqttClientWrapper.MessageReceived += HandleIncomingCommand; + if (_mqttClientWrapper.IsConnected) + { + UpdateConnectionStatus("MQTT Status: Connected"); + Log.Debug("MQTT Client Connected in InitializeMQTTConnection"); + await SetupMqttSensors(); + } + return; // Exit the method if connected + } + catch (Exception ex) + { + UpdateConnectionStatus($"MQTT Status: Disconnected (Retry {retryCount + 1})"); + + Log.Debug("MQTT Retrty Count {count} {message}", retryCount, ex.Message); + retryCount++; + await Task.Delay(2000); // Wait for 2 seconds before retrying + } + } + + UpdateConnectionStatus("MQTT Status: Disconnected (Failed to connect)"); + Log.Debug("MQTT Client Failed to Connect"); + } + + public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings settings) + { + if (_mqttClientWrapper == null) + { + Log.Debug("MQTT Client Wrapper is not initialized."); + return; + } + // Define common device information for all entities. + var deviceInfo = new + { + ids = new[] { "teams2ha_" + _deviceId }, // Unique device identifier + mf = "Jimmy White", // Manufacturer name + mdl = "Teams2HA Device", // Model + name = _deviceId, // Device name + sw = "v1.0" // Software version + }; + if (meetingUpdate == null) + { + meetingUpdate = new MeetingUpdate + { + MeetingState = new MeetingState + { + IsMuted = false, + IsVideoOn = false, + IsHandRaised = false, + IsInMeeting = false, + IsRecordingOn = false, + IsBackgroundBlurred = false, + IsSharing = false, + HasUnreadMessages = false, + teamsRunning = false + } + }; + } + foreach (var sensor in _sensorNames) + { + string sensorKey = $"{_deviceId}_{sensor}"; + string sensorName = $"{sensor}".ToLower().Replace(" ", "_"); + string deviceClass = DetermineDeviceClass(sensor); + string icon = DetermineIcon(sensor, meetingUpdate.MeetingState); + string stateValue = GetStateValue(sensor, meetingUpdate); + string uniqueId = $"{_deviceId}_{sensor}"; + + if (!_previousSensorStates.TryGetValue(sensorKey, out var previousState) || previousState != stateValue) + { + _previousSensorStates[sensorKey] = stateValue; // Update the stored state + + if (deviceClass == "switch") + { + var switchConfig = new + { + name = sensorName, + unique_id = uniqueId, + device = deviceInfo, + icon = icon, + command_topic = $"homeassistant/switch/{sensorName}/set", + state_topic = $"homeassistant/switch/{sensorName}/state", + payload_on = "ON", + payload_off = "OFF" + }; + string configTopic = $"homeassistant/switch/{sensorName}/config"; + if (!string.IsNullOrEmpty(configTopic) && switchConfig != null) + { + string switchConfigJson = JsonConvert.SerializeObject(switchConfig); + if (!string.IsNullOrEmpty(switchConfigJson)) + { + await _mqttClientWrapper.PublishAsync(configTopic, switchConfigJson, true); + } + else + { + Log.Debug($"Switch configuration JSON is null or empty for sensor: {sensor}"); + } + } + else + { + Log.Debug($"configTopic or switchConfig is null for sensor: {sensor}"); + } + await _mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(switchConfig), true); + await _mqttClientWrapper.PublishAsync(switchConfig.state_topic, stateValue); + } + else if (deviceClass == "sensor") + { + var binarySensorConfig = new + { + name = sensorName, + unique_id = uniqueId, + device = deviceInfo, + icon = icon, + state_topic = $"homeassistant/binary_sensor/{sensorName}/state", + payload_on = "true", // Assuming "True" states map to "ON" + payload_off = "false" // Assuming "False" states map to "OFF" + }; + //string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; + //await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); + //await mqttClientWrapper.PublishAsync(binarySensorConfig.state_topic, stateValue); + string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; + await _mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); + + // Here's the important part: publish the initial state for each sensor + string stateTopic = $"homeassistant/binary_sensor/{sensorName}/state"; + await _mqttClientWrapper.PublishAsync(stateTopic, stateValue.ToLowerInvariant()); // Convert "True"/"False" to "on"/"off" or keep "ON"/"OFF" + } + } + } + } + + public async Task ReconnectToMqttServerAsync() + { + // Ensure disconnection from the current MQTT server, if connected + if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) + { + await _mqttClientWrapper.DisconnectAsync(); + } + + // Attempt to connect to the MQTT server with new settings + await _mqttClientWrapper.ConnectAsync(); // Connect without checking in 'if' + + // Now, check if the connection was successful + if (_mqttClientWrapper.IsConnected) // Assuming IsConnected is a boolean property + { + StatusUpdated?.Invoke("Connected"); + await _mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + } + else + { + StatusUpdated?.Invoke("Disconnected"); + } + } + + + + public async Task SetupMqttSensors() + { + // Create a dummy MeetingUpdate with default values + var dummyMeetingUpdate = new MeetingUpdate + { + MeetingState = new MeetingState + { + IsMuted = false, + IsVideoOn = false, + IsHandRaised = false, + IsInMeeting = false, + IsRecordingOn = false, + IsBackgroundBlurred = false, + IsSharing = false, + HasUnreadMessages = false, + teamsRunning = false + } + }; + + // Call PublishConfigurations with the dummy MeetingUpdate + await PublishConfigurations(dummyMeetingUpdate, _settings); + } + + public void UpdateConnectionStatus(string status) + { + OnConnectionStatusChanged(status); + } + + #endregion Public Methods + + #region Protected Methods + + protected virtual void OnConnectionStatusChanged(string status) + { + ConnectionStatusChanged?.Invoke(status); + } + + #endregion Protected Methods + + #region Private Methods + + private string DetermineDeviceClass(string sensor) + { + switch (sensor) + { + case "IsMuted": + case "IsVideoOn": + case "IsHandRaised": + case "IsBackgroundBlurred": + return "switch"; // These are ON/OFF switches + case "IsInMeeting": + case "HasUnreadMessages": + case "IsRecordingOn": + case "IsSharing": + case "teamsRunning": + return "sensor"; // These are true/false sensors + default: + return null; // Or a default device class if appropriate + } + } + + // This method determines the appropriate icon based on the sensor and meeting state + private string DetermineIcon(string sensor, MeetingState state) + { + return sensor switch + { + // If the sensor is "IsMuted", return "mdi:microphone-off" if state.IsMuted is true, + // otherwise return "mdi:microphone" + "IsMuted" => state.IsMuted ? "mdi:microphone-off" : "mdi:microphone", + + // If the sensor is "IsVideoOn", return "mdi:camera" if state.IsVideoOn is true, + // otherwise return "mdi:camera-off" + "IsVideoOn" => state.IsVideoOn ? "mdi:camera" : "mdi:camera-off", + + // If the sensor is "IsHandRaised", return "mdi:hand-back-left" if + // state.IsHandRaised is true, otherwise return "mdi:hand-back-left-off" + "IsHandRaised" => state.IsHandRaised ? "mdi:hand-back-left" : "mdi:hand-back-left-off", + + // If the sensor is "IsInMeeting", return "mdi:account-group" if state.IsInMeeting + // is true, otherwise return "mdi:account-off" + "IsInMeeting" => state.IsInMeeting ? "mdi:account-group" : "mdi:account-off", + + // If the sensor is "IsRecordingOn", return "mdi:record-rec" if state.IsRecordingOn + // is true, otherwise return "mdi:record" + "IsRecordingOn" => state.IsRecordingOn ? "mdi:record-rec" : "mdi:record", + + // If the sensor is "IsBackgroundBlurred", return "mdi:blur" if + // state.IsBackgroundBlurred is true, otherwise return "mdi:blur-off" + "IsBackgroundBlurred" => state.IsBackgroundBlurred ? "mdi:blur" : "mdi:blur-off", + + // If the sensor is "IsSharing", return "mdi:monitor-share" if state.IsSharing is + // true, otherwise return "mdi:monitor-off" + "IsSharing" => state.IsSharing ? "mdi:monitor-share" : "mdi:monitor-off", + + // If the sensor is "HasUnreadMessages", return "mdi:message-alert" if + // state.HasUnreadMessages is true, otherwise return "mdi:message-outline" + "HasUnreadMessages" => state.HasUnreadMessages ? "mdi:message-alert" : "mdi:message-outline", + + // If the sensor does not match any of the above cases, return "mdi:eye" + _ => "mdi:eye" + }; + } + private void UpdateMqttClientWrapper() + { + + _mqttClientWrapper = new MqttClientWrapper( + "TEAMS2HA", + _settings.MqttAddress, + _settings.MqttPort, + _settings.MqttUsername, + _settings.MqttPassword, + _settings.UseTLS, + _settings.IgnoreCertificateErrors, + _settings.UseWebsockets + ); + // Subscribe to the ConnectionStatusChanged event + _mqttClientWrapper.ConnectionStatusChanged += UpdateConnectionStatus; + } + private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) + { + switch (sensor) + { + case "IsMuted": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsVideoOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsBackgroundBlurred": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsHandRaised": + // Cast to bool and then check the value + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsInMeeting": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "HasUnreadMessages": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "IsRecordingOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "IsSharing": + // Similar casting for these properties + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "teamsRunning": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + default: + return "unknown"; + } + } + + #endregion Private Methods + + // Additional extracted methods... + } +} \ No newline at end of file diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index a62a5a6..bfae767 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -220,7 +220,7 @@ public partial class MainWindow : Window }; private bool teamspaired = false; - + private MqttManager _mqttManager; #endregion Private Fields #region Public Constructors @@ -230,6 +230,8 @@ public MainWindow() { // Get the local application data folder path var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + + // Configure logging LoggingConfig.Configure(); @@ -281,7 +283,10 @@ public MainWindow() _settings.IgnoreCertificateErrors, _settings.UseWebsockets ); - + _mqttManager = new MqttManager(mqttClientWrapper, settings, sensorNames, deviceid); + _mqttManager.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; + _mqttManager.StatusUpdated += UpdateMqttStatus; + _mqttManager.CommandToTeams += HandleCommandToTeams; // Set the action to be performed when a new token is updated _updateTokenAction = newToken => { @@ -304,7 +309,7 @@ public MainWindow() mqttKeepAliveTimer.Elapsed += OnTimedEvent; mqttKeepAliveTimer.AutoReset = true; mqttKeepAliveTimer.Enabled = true; - + // Initialize the MQTT publish timer InitializeMqttPublishTimer(); @@ -316,63 +321,13 @@ public MainWindow() public async Task InitializeConnections() { - await InitializeMQTTConnection(); + await _mqttManager.InitializeConnection(); // Other initialization code... await initializeteamsconnection(); // Other initialization code... } - public async Task InitializeMQTTConnection() - { - if (mqttClientWrapper == null) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Client Not Initialized"); - Log.Debug("MQTT Client Not Initialized"); - return; - - } - //check we have at least an mqtt server address - if (string.IsNullOrEmpty(_settings.MqttAddress)) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Server Address Not Set"); - Log.Debug("MQTT Server Address Not Set"); - return; - } - 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"); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - - mqttClientWrapper.MessageReceived += HandleIncomingCommand; - if (mqttClientWrapper.IsConnected) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); - Log.Debug("MQTT Client Connected in InitializeMQTTConnection"); - await SetupMqttSensors(); - } - return; // Exit the method if connected - } - catch (Exception ex) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = $"MQTT Status: Disconnected (Retry {retryCount + 1})"); - - Log.Debug("MQTT Retrty Count {count} {message}", retryCount, ex.Message); - retryCount++; - await Task.Delay(2000); // Wait for 2 seconds before retrying - } - } - - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Disconnected (Failed to connect)"); - Log.Debug("MQTT Client Failed to Connect"); - } - #endregion Public Methods #region Protected Methods @@ -419,41 +374,6 @@ private void UpdateMqttConnectionStatus(string status) Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); Dispatcher.Invoke(() => UpdateStatusMenuItems()); } - private void UpdateMqttClientWrapper() - { - - mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword, - _settings.UseTLS, - _settings.IgnoreCertificateErrors, - _settings.UseWebsockets - ); - - - // 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(); - } - - // Update the MQTT client wrapper with new settings - UpdateMqttClientWrapper(); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - // Attempt to connect to the MQTT server with new settings - await mqttClientWrapper.ConnectAsync(); - //we need to subscribe again (Thanks to @egglestron for pointing this out!) - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - } - private void SetWindowTitle() { @@ -469,6 +389,29 @@ private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) ReestablishConnections(); } } + private void UpdateMqttStatus(string status) + { + Dispatcher.Invoke(() => + { + // Assuming MQTTConnectionStatus is a Label or similar control + MQTTConnectionStatus.Text = $"MQTT Status: {status}"; + UpdateStatusMenuItems(); + }); + } + private void MqttManager_ConnectionStatusChanged(string status) + { + Dispatcher.Invoke(() => + { + MQTTConnectionStatus.Text = status; // Ensure MQTTConnectionStatus is the correct UI element's name + }); + } + private async Task HandleCommandToTeams(string jsonMessage) + { + if (_teamsClient != null) + { + await _teamsClient.SendMessageAsync(jsonMessage); + } + } private async void ReestablishConnections() { try @@ -477,7 +420,7 @@ private async void ReestablishConnections() { await mqttClientWrapper.ConnectAsync(); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - await SetupMqttSensors(); + await _mqttManager.SetupMqttSensors(); Dispatcher.Invoke(() => UpdateStatusMenuItems()); } if (!_teamsClient.IsConnected) @@ -492,6 +435,7 @@ private async void ReestablishConnections() } } + private void CreateNotifyIconContextMenu() { ContextMenu contextMenu = new ContextMenu(); @@ -619,149 +563,13 @@ private void UpdateConnectionStatus() } - private string DetermineDeviceClass(string sensor) - { - switch (sensor) - { - case "IsMuted": - case "IsVideoOn": - case "IsHandRaised": - case "IsBackgroundBlurred": - return "switch"; // These are ON/OFF switches - case "IsInMeeting": - case "HasUnreadMessages": - case "IsRecordingOn": - case "IsSharing": - case "teamsRunning": - return "sensor"; // These are true/false sensors - default: - return null; // Or a default device class if appropriate - } - } - - // This method determines the appropriate icon based on the sensor and meeting state - private string DetermineIcon(string sensor, MeetingState state) - { - return sensor switch - { - // If the sensor is "IsMuted", return "mdi:microphone-off" if state.IsMuted is true, - // otherwise return "mdi:microphone" - "IsMuted" => state.IsMuted ? "mdi:microphone-off" : "mdi:microphone", - - // If the sensor is "IsVideoOn", return "mdi:camera" if state.IsVideoOn is true, - // otherwise return "mdi:camera-off" - "IsVideoOn" => state.IsVideoOn ? "mdi:camera" : "mdi:camera-off", - - // If the sensor is "IsHandRaised", return "mdi:hand-back-left" if - // state.IsHandRaised is true, otherwise return "mdi:hand-back-left-off" - "IsHandRaised" => state.IsHandRaised ? "mdi:hand-back-left" : "mdi:hand-back-left-off", - - // If the sensor is "IsInMeeting", return "mdi:account-group" if state.IsInMeeting - // is true, otherwise return "mdi:account-off" - "IsInMeeting" => state.IsInMeeting ? "mdi:account-group" : "mdi:account-off", - - // If the sensor is "IsRecordingOn", return "mdi:record-rec" if state.IsRecordingOn - // is true, otherwise return "mdi:record" - "IsRecordingOn" => state.IsRecordingOn ? "mdi:record-rec" : "mdi:record", - - // If the sensor is "IsBackgroundBlurred", return "mdi:blur" if - // state.IsBackgroundBlurred is true, otherwise return "mdi:blur-off" - "IsBackgroundBlurred" => state.IsBackgroundBlurred ? "mdi:blur" : "mdi:blur-off", - - // If the sensor is "IsSharing", return "mdi:monitor-share" if state.IsSharing is - // true, otherwise return "mdi:monitor-off" - "IsSharing" => state.IsSharing ? "mdi:monitor-share" : "mdi:monitor-off", - - // If the sensor is "HasUnreadMessages", return "mdi:message-alert" if - // state.HasUnreadMessages is true, otherwise return "mdi:message-outline" - "HasUnreadMessages" => state.HasUnreadMessages ? "mdi:message-alert" : "mdi:message-outline", - - // If the sensor does not match any of the above cases, return "mdi:eye" - _ => "mdi:eye" - - }; - } - - private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) - { - switch (sensor) - { - case "IsMuted": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - case "IsVideoOn": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - case "IsBackgroundBlurred": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - case "IsHandRaised": - // Cast to bool and then check the value - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - - case "IsInMeeting": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - case "HasUnreadMessages": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - case "IsRecordingOn": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - case "IsSharing": - // Similar casting for these properties - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - case "teamsRunning": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - default: - return "unknown"; - } - } - - private async Task HandleIncomingCommand(MqttApplicationMessageReceivedEventArgs e) - { - string topic = e.ApplicationMessage.Topic; - Log.Debug("HandleIncomingCommand: MQTT Topic {topic}", topic); - // 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.PayloadSegment); - // Parse and handle the command - HandleSwitchCommand(topic, command); - } - } - - private async void HandleSwitchCommand(string topic, string command) - { - // Determine which switch is being controlled based on the topic - string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" - int underscoreIndex = switchName.IndexOf('_'); - if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) - { - switchName = switchName.Substring(underscoreIndex + 1); - } - string jsonMessage = ""; - switch (switchName) - { - case "ismuted": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "isvideoon": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; + - case "isbackgroundblurred": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - case "ishandraised": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; + - // Add other cases as needed - } - if (!string.IsNullOrEmpty(jsonMessage)) - { - // Send the message to Teams - await _teamsClient.SendMessageAsync(jsonMessage); - } - } + private void InitializeMqttPublishTimer() { @@ -940,91 +748,7 @@ private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) CheckMqttConnection(); } - private async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings settings) - { - // Define common device information for all entities. - var deviceInfo = new - { - ids = new[] { "teams2ha_" + deviceid }, // Unique device identifier - mf = "Jimmy White", // Manufacturer name - mdl = "Teams2HA Device", // Model - name = deviceid, // Device name - sw = "v1.0" // Software version - }; - if (meetingUpdate == null) - { - meetingUpdate = new MeetingUpdate - { - MeetingState = new MeetingState - { - IsMuted = false, - IsVideoOn = false, - IsHandRaised = false, - IsInMeeting = false, - IsRecordingOn = false, - IsBackgroundBlurred = false, - IsSharing = false, - HasUnreadMessages = false, - teamsRunning = false - - } - }; - } - foreach (var sensor in sensorNames) - { - string sensorKey = $"{deviceid}_{sensor}"; - string sensorName = $"{sensor}".ToLower().Replace(" ", "_"); - string deviceClass = DetermineDeviceClass(sensor); - string icon = DetermineIcon(sensor, meetingUpdate.MeetingState); - string stateValue = GetStateValue(sensor, meetingUpdate); - string uniqueId = $"{deviceid}_{sensor}"; - - if (!_previousSensorStates.TryGetValue(sensorKey, out var previousState) || previousState != stateValue) - { - _previousSensorStates[sensorKey] = stateValue; // Update the stored state - - if (deviceClass == "switch") - { - var switchConfig = new - { - name = sensorName, - unique_id = uniqueId, - device = deviceInfo, - icon = icon, - command_topic = $"homeassistant/switch/{sensorName}/set", - state_topic = $"homeassistant/switch/{sensorName}/state", - payload_on = "ON", - payload_off = "OFF" - }; - string configTopic = $"homeassistant/switch/{sensorName}/config"; - await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(switchConfig), true); - await mqttClientWrapper.PublishAsync(switchConfig.state_topic, stateValue); - } - else if (deviceClass == "sensor") - { - var binarySensorConfig = new - { - name = sensorName, - unique_id = uniqueId, - device = deviceInfo, - icon = icon, - state_topic = $"homeassistant/binary_sensor/{sensorName}/state", - payload_on = "true", // Assuming "True" states map to "ON" - payload_off = "false" // Assuming "False" states map to "OFF" - }; - //string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; - //await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); - //await mqttClientWrapper.PublishAsync(binarySensorConfig.state_topic, stateValue); - string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; - await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); - - // Here's the important part: publish the initial state for each sensor - string stateTopic = $"homeassistant/binary_sensor/{sensorName}/state"; - await mqttClientWrapper.PublishAsync(stateTopic, stateValue.ToLowerInvariant()); // Convert "True"/"False" to "on"/"off" or keep "ON"/"OFF" - } - } - } - } + @@ -1064,9 +788,9 @@ private async Task SaveSettingsAsync() settings.SaveSettingsToFile(); - await ReconnectToMqttServerAsync(); - await PublishConfigurations(_latestMeetingUpdate, _settings); - await SetupMqttSensors(); + await _mqttManager.ReconnectToMqttServerAsync(); + await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); + await _mqttManager.SetupMqttSensors(); @@ -1088,30 +812,7 @@ private async Task SetStartupAsync(bool startWithWindows) } - private async Task SetupMqttSensors() - { - // Create a dummy MeetingUpdate with default values - var dummyMeetingUpdate = new MeetingUpdate - { - MeetingState = new MeetingState - { - IsMuted = false, - IsVideoOn = false, - IsHandRaised = false, - IsInMeeting = false, - IsRecordingOn = false, - IsBackgroundBlurred = false, - IsSharing = false, - HasUnreadMessages = false, - teamsRunning = false - } - }; - - // Call PublishConfigurations with the dummy MeetingUpdate - await PublishConfigurations(dummyMeetingUpdate, _settings); - - - } + private async void TeamsClient_TeamsUpdateReceived(object sender, WebSocketClient.TeamsUpdateEventArgs e) { @@ -1121,7 +822,7 @@ private async void TeamsClient_TeamsUpdateReceived(object sender, WebSocketClien _latestMeetingUpdate = e.MeetingUpdate; Log.Debug("TeamsClient_TeamsUpdateReceived: Teams Update Received {update}", _latestMeetingUpdate); // Update sensor configurations - await PublishConfigurations(_latestMeetingUpdate, _settings); + await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); } } @@ -1140,7 +841,7 @@ private void TeamsConnectionStatusChanged(bool isConnected) else { State.Instance.teamsRunning = false; - _= PublishConfigurations(null, _settings); + _= _mqttManager.PublishConfigurations(null, _settings); } Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 7387e3d..54bfdd5 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.373 - 1.1.0.373 + 1.1.0.381 + 1.1.0.381 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 351dc1ad638ea8eb31c8af2591f3e2f22703d37a Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 17:22:37 +0000 Subject: [PATCH 08/31] more refasctoring --- API/MqttManager.cs | 25 +++++++++++++++++++++++-- MainWindow.xaml.cs | 34 ++++++++-------------------------- TEAMS2HA.csproj | 4 ++-- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/API/MqttManager.cs b/API/MqttManager.cs index 6cc1ff5..c4823e8 100644 --- a/API/MqttManager.cs +++ b/API/MqttManager.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using System.Windows.Threading; using Newtonsoft.Json; +using System.Timers; namespace TEAMS2HA.API { @@ -23,6 +24,7 @@ public class MqttManager public event Action StatusUpdated; public delegate Task CommandToTeamsHandler(string jsonMessage); public event CommandToTeamsHandler CommandToTeams; + private System.Timers.Timer mqttPublishTimer; #endregion Private Fields #region Public Constructors @@ -35,6 +37,7 @@ public MqttManager(MqttClientWrapper mqttClientWrapper, AppSettings settings, Li _deviceId = deviceId; _previousSensorStates = new Dictionary(); InitializeConnection(); + InitializeMqttPublishTimer(); } #endregion Public Constructors @@ -322,7 +325,18 @@ protected virtual void OnConnectionStatusChanged(string status) #endregion Protected Methods #region Private Methods - + private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) + { + if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) + { + // Example: Publish a keep-alive message + string keepAliveTopic = "TEAMS2HA/keepalive"; + string keepAliveMessage = "alive"; + _ = _mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); + Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); + + } + } private string DetermineDeviceClass(string sensor) { switch (sensor) @@ -342,7 +356,14 @@ private string DetermineDeviceClass(string sensor) return null; // Or a default device class if appropriate } } - + public void InitializeMqttPublishTimer() + { + mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds + mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; + mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses + mqttPublishTimer.Enabled = true; // Enable the timer + Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); + } // This method determines the appropriate icon based on the sensor and meeting state private string DetermineIcon(string sensor, MeetingState state) { diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index bfae767..573d407 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -210,7 +210,7 @@ public partial class MainWindow : Window private bool isDarkTheme = false; private MqttClientWrapper mqttClientWrapper; private System.Timers.Timer mqttKeepAliveTimer; - private System.Timers.Timer mqttPublishTimer; + private string Mqtttopic; private Dictionary _previousSensorStates = new Dictionary(); @@ -305,13 +305,13 @@ public MainWindow() } // Create a timer for MQTT keep alive - mqttKeepAliveTimer = new System.Timers.Timer(60000); // Set interval to 60 seconds (60000 ms) - mqttKeepAliveTimer.Elapsed += OnTimedEvent; - mqttKeepAliveTimer.AutoReset = true; - mqttKeepAliveTimer.Enabled = true; + //mqttKeepAliveTimer = new System.Timers.Timer(60000); // Set interval to 60 seconds (60000 ms) + //mqttKeepAliveTimer.Elapsed += OnTimedEvent; + //mqttKeepAliveTimer.AutoReset = true; + //mqttKeepAliveTimer.Enabled = true; // Initialize the MQTT publish timer - InitializeMqttPublishTimer(); + _mqttManager.InitializeMqttPublishTimer(); } @@ -571,14 +571,7 @@ private void UpdateConnectionStatus() - private void InitializeMqttPublishTimer() - { - mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds - mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; - mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses - mqttPublishTimer.Enabled = true; // Enable the timer - Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); - } + private async Task initializeteamsconnection() { @@ -729,18 +722,7 @@ private void MyNotifyIcon_Click(object sender, EventArgs e) } } - private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) - { - if (mqttClientWrapper != null && mqttClientWrapper.IsConnected) - { - // Example: Publish a keep-alive message - string keepAliveTopic = "TEAMS2HA/keepalive"; - string keepAliveMessage = "alive"; - _ = mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); - Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - } - } + private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) { diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 54bfdd5..201f446 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.381 - 1.1.0.381 + 1.1.0.382 + 1.1.0.382 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 4cdb1d45896a8e2968392908e89f6347a060e4fc Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 17:44:10 +0000 Subject: [PATCH 09/31] reconnection logic --- API/MqttClientWrapper.cs | 60 ++++++++++++++++++++++++++++++++++++++-- API/MqttManager.cs | 9 ++++++ MainWindow.xaml.cs | 7 ++--- TEAMS2HA.csproj | 4 +-- 4 files changed, 70 insertions(+), 10 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index 5d0b195..f366dc2 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -23,7 +23,7 @@ public class MqttClientWrapper private MqttClientOptions _mqttOptions; private bool _isAttemptingConnection = false; private const int MaxConnectionRetries = 5; - private const int RetryDelayMilliseconds = 2000; //wait a couple of seconds before retrying a connection attempt + private const int RetryDelayMilliseconds = 1000; //wait a couple of seconds before retrying a connection attempt #endregion Private Fields public event Action ConnectionStatusChanged; @@ -263,9 +263,63 @@ public async Task PublishAsync(string topic, string payload, bool retain = true) // Depending on the severity, you might want to rethrow the exception or handle it here. } } - + public void UpdateClientSettings(string mqttBroker, string mqttPort, string username, string password, bool useTLS, bool ignoreCertificateErrors, bool useWebsockets) + { + // Convert the MQTT port from string to integer, defaulting to 1883 if conversion fails + if (!int.TryParse(mqttPort, out int portNumber)) + { + portNumber = 1883; // Default MQTT port + Log.Warning($"Invalid MQTT port provided, defaulting to {portNumber}"); + } + + // Start building the new MQTT client options + var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() + .WithClientId(Guid.NewGuid().ToString()) // Use a new client ID for a new connection + .WithCredentials(username, password) + .WithCleanSession(); + + // Setup connection type: WebSockets or TCP + if (useWebsockets) + { + string websocketUri = useTLS ? $"wss://{mqttBroker}:{portNumber}" : $"ws://{mqttBroker}:{portNumber}"; + mqttClientOptionsBuilder.WithWebSocketServer(websocketUri); + Log.Information($"Updating MQTT client settings for WebSocket {(useTLS ? "with TLS" : "without TLS")} connection to {websocketUri}"); + } + else + { + mqttClientOptionsBuilder.WithTcpServer(mqttBroker, portNumber); + Log.Information($"Updating MQTT client settings for TCP {(useTLS ? "with TLS" : "without TLS")} connection to {mqttBroker}:{portNumber}"); + } + + // Setup TLS/SSL settings if needed + if (useTLS) + { + mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters + { + UseTls = true, + AllowUntrustedCertificates = ignoreCertificateErrors, + IgnoreCertificateChainErrors = ignoreCertificateErrors, + IgnoreCertificateRevocationErrors = ignoreCertificateErrors, + CertificateValidationHandler = context => + { + // Implement your TLS validation logic here + // Log any details necessary and return true if validation is successful + // For simplicity and security example, this will return true if ignoreCertificateErrors is true + return ignoreCertificateErrors; // WARNING: Setting this to always 'true' might pose a security risk + } + }); + } + + // Apply the new settings to the MQTT client + _mqttOptions = mqttClientOptionsBuilder.Build(); + + // If needed, log the new settings or perform any other necessary actions here + Log.Information("MQTT client settings updated successfully."); + } + + - public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) + public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) { var subscribeOptions = new MqttClientSubscribeOptionsBuilder() .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) diff --git a/API/MqttManager.cs b/API/MqttManager.cs index c4823e8..bd5b06d 100644 --- a/API/MqttManager.cs +++ b/API/MqttManager.cs @@ -262,6 +262,15 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings public async Task ReconnectToMqttServerAsync() { + _mqttClientWrapper.UpdateClientSettings( + _settings.MqttAddress, + _settings.MqttPort, + _settings.MqttUsername, + _settings.MqttPassword, + _settings.UseTLS, + _settings.IgnoreCertificateErrors, + _settings.UseWebsockets); + // Ensure disconnection from the current MQTT server, if connected if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) { diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 573d407..a003904 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -211,7 +211,7 @@ public partial class MainWindow : Window private MqttClientWrapper mqttClientWrapper; private System.Timers.Timer mqttKeepAliveTimer; - private string Mqtttopic; + //private string Mqtttopic; private Dictionary _previousSensorStates = new Dictionary(); private List sensorNames = new List @@ -769,12 +769,9 @@ private async Task SaveSettingsAsync() // Save the updated settings to file settings.SaveSettingsToFile(); - await _mqttManager.ReconnectToMqttServerAsync(); - await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); await _mqttManager.SetupMqttSensors(); - - + await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); } diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 201f446..ed789bc 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.382 - 1.1.0.382 + 1.1.0.387 + 1.1.0.387 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From d1d549ae37f833655bff723af468d36111c316bf Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 17:55:52 +0000 Subject: [PATCH 10/31] mqtt connection timeout option --- API/MqttClientWrapper.cs | 13 +++++++++---- TEAMS2HA.csproj | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index f366dc2..bf5fff9 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -22,7 +22,7 @@ public class MqttClientWrapper private MqttClient _mqttClient; private MqttClientOptions _mqttOptions; private bool _isAttemptingConnection = false; - private const int MaxConnectionRetries = 5; + private const int MaxConnectionRetries = 2; private const int RetryDelayMilliseconds = 1000; //wait a couple of seconds before retrying a connection attempt #endregion Private Fields @@ -52,7 +52,8 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() .WithClientId(clientId) .WithCredentials(username, password) - .WithCleanSession(); + .WithCleanSession() + .WithTimeout(TimeSpan.FromSeconds(5)); string protocol = useWebsockets ? "ws" : "tcp"; string connectionType = useTLS ? "with TLS" : "without TLS"; @@ -276,7 +277,9 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() .WithClientId(Guid.NewGuid().ToString()) // Use a new client ID for a new connection .WithCredentials(username, password) - .WithCleanSession(); + .WithCleanSession() + .WithTimeout(TimeSpan.FromSeconds(5)); + // Setup connection type: WebSockets or TCP if (useWebsockets) @@ -297,6 +300,8 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters { UseTls = true, + // need to set the timeout for connections + AllowUntrustedCertificates = ignoreCertificateErrors, IgnoreCertificateChainErrors = ignoreCertificateErrors, IgnoreCertificateRevocationErrors = ignoreCertificateErrors, @@ -309,7 +314,7 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user } }); } - + // Apply the new settings to the MQTT client _mqttOptions = mqttClientOptionsBuilder.Build(); diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index ed789bc..c893f0f 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.387 - 1.1.0.387 + 1.1.0.391 + 1.1.0.391 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From df5608703d31aac88cd9a6a3f1a810efcf154a72 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 17:57:45 +0000 Subject: [PATCH 11/31] clean up --- API/MqttClientWrapper.cs | 147 +++++++------ API/MqttManager.cs | 154 ++++++------- MainWindow.xaml.cs | 458 ++++++++++++++++++--------------------- 3 files changed, 372 insertions(+), 387 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index bf5fff9..2003eb4 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -19,21 +19,22 @@ public class MqttClientWrapper { #region Private Fields + private const int MaxConnectionRetries = 2; + private const int RetryDelayMilliseconds = 1000; + private bool _isAttemptingConnection = false; private MqttClient _mqttClient; private MqttClientOptions _mqttOptions; - private bool _isAttemptingConnection = false; - private const int MaxConnectionRetries = 2; - private const int RetryDelayMilliseconds = 1000; //wait a couple of seconds before retrying a connection attempt + //wait a couple of seconds before retrying a connection attempt #endregion Private Fields + + #region Public Events + public event Action ConnectionStatusChanged; + #endregion Public Events + #region Public Constructors - public bool IsAttemptingConnection - { - get { return _isAttemptingConnection; } - private set { _isAttemptingConnection = value; } - } [Obsolete] public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, string username, string password, bool useTLS, bool ignoreCertificateErrors, bool useWebsockets) @@ -84,8 +85,8 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st Log.Debug("Certificate Subject: {0}", context.Certificate.Subject); // This assumes you are trying to inspect the certificate directly; - // MQTTnet may not provide a direct IsValid flag or ChainErrors like .NET's X509Chain. - // Instead, you handle validation and log details manually: + // MQTTnet may not provide a direct IsValid flag or ChainErrors like + // .NET's X509Chain. Instead, you handle validation and log details manually: bool isValid = true; // You should define the logic to set this based on your validation requirements @@ -104,8 +105,9 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st isValid = false; // Consider invalid if there are any SSL policy errors } - // You can decide to ignore certain errors by setting isValid to true regardless of the checks, - // but be careful as this might introduce security vulnerabilities. + // You can decide to ignore certain errors by setting isValid to true + // regardless of the checks, but be careful as this might introduce + // security vulnerabilities. if (ignoreCertificateErrors) { isValid = true; // Ignore certificate errors if your settings dictate @@ -113,7 +115,6 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st return isValid; // Return the result of your checks } - }); } @@ -130,8 +131,16 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st } } + public bool IsAttemptingConnection + { + get { return _isAttemptingConnection; } + private set { _isAttemptingConnection = value; } + } + #endregion Public Constructors + + #region Public Events public event Func MessageReceived; @@ -146,6 +155,24 @@ public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, st #region Public Methods + public static List GetEntityNames(string deviceId) + { + var entityNames = new List + { + $"switch.{deviceId}_ismuted", + $"switch.{deviceId}_isvideoon", + $"switch.{deviceId}_ishandraised", + $"sensor.{deviceId}_isrecordingon", + $"sensor.{deviceId}_isinmeeting", + $"sensor.{deviceId}_issharing", + $"sensor.{deviceId}_hasunreadmessages", + $"switch.{deviceId}_isbackgroundblurred", + $"sensor.{deviceId}_teamsRunning" + }; + + return entityNames; + } + public async Task ConnectAsync() { if (_mqttClient.IsConnected || _isAttemptingConnection) @@ -217,26 +244,8 @@ public void Dispose() Log.Information("MQTT Client Disposed"); } } - public static List GetEntityNames(string deviceId) - { - var entityNames = new List - { - $"switch.{deviceId}_ismuted", - $"switch.{deviceId}_isvideoon", - $"switch.{deviceId}_ishandraised", - $"sensor.{deviceId}_isrecordingon", - $"sensor.{deviceId}_isinmeeting", - $"sensor.{deviceId}_issharing", - $"sensor.{deviceId}_hasunreadmessages", - $"switch.{deviceId}_isbackgroundblurred", - $"sensor.{deviceId}_teamsRunning" - }; - - return entityNames; - } - -public async Task PublishAsync(string topic, string payload, bool retain = true) + public async Task PublishAsync(string topic, string payload, bool retain = true) { try { @@ -264,6 +273,24 @@ public async Task PublishAsync(string topic, string payload, bool retain = true) // Depending on the severity, you might want to rethrow the exception or handle it here. } } + + public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) + { + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) + .Build(); + try + { + await _mqttClient.SubscribeAsync(subscribeOptions); + } + catch (Exception ex) + { + Log.Information($"Error during MQTT subscribe: {ex.Message}"); + // Depending on the severity, you might want to rethrow the exception or handle it here. + } + Log.Information("Subscribing." + subscribeOptions); + } + public void UpdateClientSettings(string mqttBroker, string mqttPort, string username, string password, bool useTLS, bool ignoreCertificateErrors, bool useWebsockets) { // Convert the MQTT port from string to integer, defaulting to 1883 if conversion fails @@ -279,7 +306,6 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user .WithCredentials(username, password) .WithCleanSession() .WithTimeout(TimeSpan.FromSeconds(5)); - // Setup connection type: WebSockets or TCP if (useWebsockets) @@ -307,14 +333,14 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user IgnoreCertificateRevocationErrors = ignoreCertificateErrors, CertificateValidationHandler = context => { - // Implement your TLS validation logic here - // Log any details necessary and return true if validation is successful - // For simplicity and security example, this will return true if ignoreCertificateErrors is true + // Implement your TLS validation logic here Log any details necessary and + // return true if validation is successful For simplicity and security + // example, this will return true if ignoreCertificateErrors is true return ignoreCertificateErrors; // WARNING: Setting this to always 'true' might pose a security risk } }); } - + // Apply the new settings to the MQTT client _mqttOptions = mqttClientOptionsBuilder.Build(); @@ -322,47 +348,28 @@ public void UpdateClientSettings(string mqttBroker, string mqttPort, string user Log.Information("MQTT client settings updated successfully."); } + #endregion Public Methods + #region Private Methods - public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) - { - var subscribeOptions = new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) - .Build(); - try + private async Task HandleReceivedApplicationMessage(MqttApplicationMessageReceivedEventArgs e) { - await _mqttClient.SubscribeAsync(subscribeOptions); - } - catch (Exception ex) - { - Log.Information($"Error during MQTT subscribe: {ex.Message}"); - // Depending on the severity, you might want to rethrow the exception or handle it here. + if (MessageReceived != null) + { + await MessageReceived(e); + Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); + } } - Log.Information("Subscribing." + subscribeOptions); - } - - #endregion Public Methods - #region Private Methods - - private async Task HandleReceivedApplicationMessage(MqttApplicationMessageReceivedEventArgs e) - { - if (MessageReceived != null) + private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) { - await MessageReceived(e); Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); - } - } + // Trigger the event to notify subscribers + MessageReceived?.Invoke(e); - private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) - { - Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); - // Trigger the event to notify subscribers - MessageReceived?.Invoke(e); + return Task.CompletedTask; + } - return Task.CompletedTask; + #endregion Private Methods } - - #endregion Private Methods -} } \ No newline at end of file diff --git a/API/MqttManager.cs b/API/MqttManager.cs index bd5b06d..79e1919 100644 --- a/API/MqttManager.cs +++ b/API/MqttManager.cs @@ -21,10 +21,14 @@ public class MqttManager private readonly AppSettings _settings; private MqttClientWrapper _mqttClientWrapper; private Dictionary _previousSensorStates; - public event Action StatusUpdated; + private System.Timers.Timer mqttPublishTimer; + public delegate Task CommandToTeamsHandler(string jsonMessage); + public event CommandToTeamsHandler CommandToTeams; - private System.Timers.Timer mqttPublishTimer; + + public event Action StatusUpdated; + #endregion Private Fields #region Public Constructors @@ -68,43 +72,7 @@ public async Task HandleIncomingCommand(MqttApplicationMessageReceivedEventArgs HandleSwitchCommand(topic, command); } } - private async void HandleSwitchCommand(string topic, string command) - { - // Determine which switch is being controlled based on the topic - string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" - int underscoreIndex = switchName.IndexOf('_'); - if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) - { - switchName = switchName.Substring(underscoreIndex + 1); - } - string jsonMessage = ""; - switch (switchName) - { - case "ismuted": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "isvideoon": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - case "isbackgroundblurred": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "ishandraised": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - // Add other cases as needed - } - - if (!string.IsNullOrEmpty(jsonMessage)) - { - // Raise the event - CommandToTeams?.Invoke(jsonMessage); - } - } public async Task InitializeConnection() { if (_mqttClientWrapper == null) @@ -292,8 +260,6 @@ public async Task ReconnectToMqttServerAsync() } } - - public async Task SetupMqttSensors() { // Create a dummy MeetingUpdate with default values @@ -322,6 +288,44 @@ public void UpdateConnectionStatus(string status) OnConnectionStatusChanged(status); } + private async void HandleSwitchCommand(string topic, string command) + { + // Determine which switch is being controlled based on the topic + string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" + int underscoreIndex = switchName.IndexOf('_'); + if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) + { + switchName = switchName.Substring(underscoreIndex + 1); + } + string jsonMessage = ""; + switch (switchName) + { + case "ismuted": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isvideoon": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isbackgroundblurred": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "ishandraised": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + // Add other cases as needed + } + + if (!string.IsNullOrEmpty(jsonMessage)) + { + // Raise the event + CommandToTeams?.Invoke(jsonMessage); + } + } + #endregion Public Methods #region Protected Methods @@ -334,18 +338,16 @@ protected virtual void OnConnectionStatusChanged(string status) #endregion Protected Methods #region Private Methods - private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) + + public void InitializeMqttPublishTimer() { - if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) - { - // Example: Publish a keep-alive message - string keepAliveTopic = "TEAMS2HA/keepalive"; - string keepAliveMessage = "alive"; - _ = _mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); - Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); - - } + mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds + mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; + mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses + mqttPublishTimer.Enabled = true; // Enable the timer + Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); } + private string DetermineDeviceClass(string sensor) { switch (sensor) @@ -365,14 +367,7 @@ private string DetermineDeviceClass(string sensor) return null; // Or a default device class if appropriate } } - public void InitializeMqttPublishTimer() - { - mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds - mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; - mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses - mqttPublishTimer.Enabled = true; // Enable the timer - Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); - } + // This method determines the appropriate icon based on the sensor and meeting state private string DetermineIcon(string sensor, MeetingState state) { @@ -414,22 +409,7 @@ private string DetermineIcon(string sensor, MeetingState state) _ => "mdi:eye" }; } - private void UpdateMqttClientWrapper() - { - _mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword, - _settings.UseTLS, - _settings.IgnoreCertificateErrors, - _settings.UseWebsockets - ); - // Subscribe to the ConnectionStatusChanged event - _mqttClientWrapper.ConnectionStatusChanged += UpdateConnectionStatus; - } private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) { switch (sensor) @@ -468,6 +448,34 @@ private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) } } + private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) + { + if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) + { + // Example: Publish a keep-alive message + string keepAliveTopic = "TEAMS2HA/keepalive"; + string keepAliveMessage = "alive"; + _ = _mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); + Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); + } + } + + private void UpdateMqttClientWrapper() + { + _mqttClientWrapper = new MqttClientWrapper( + "TEAMS2HA", + _settings.MqttAddress, + _settings.MqttPort, + _settings.MqttUsername, + _settings.MqttPassword, + _settings.UseTLS, + _settings.IgnoreCertificateErrors, + _settings.UseWebsockets + ); + // Subscribe to the ConnectionStatusChanged event + _mqttClientWrapper.ConnectionStatusChanged += UpdateConnectionStatus; + } + #endregion Private Methods // Additional extracted methods... diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index a003904..7e36000 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -76,30 +76,29 @@ public static AppSettings Instance } } - [JsonIgnore] - public string MqttPassword { get; set; } - public bool UseWebsockets { get; set; } - [JsonIgnore] - public string PlainTeamsToken { get; set; } // Properties public string EncryptedMqttPassword { get; set; } + public bool IgnoreCertificateErrors { get; set; } + public string MqttAddress { get; set; } - public string MqttPort { get; set; } - public string SensorPrefix { get; set; } + [JsonIgnore] + public string MqttPassword { get; set; } + public string MqttPort { get; set; } public string MqttUsername { get; set; } - public bool RunAtWindowsBoot { get; set; } - public bool UseTLS { get; set; } - public bool IgnoreCertificateErrors { get; set; } + [JsonIgnore] + public string PlainTeamsToken { get; set; } + public bool RunAtWindowsBoot { get; set; } public bool RunMinimized { get; set; } - + public string SensorPrefix { get; set; } public string TeamsToken { get; set; } - public string Theme { get; set; } + public bool UseTLS { get; set; } + public bool UseWebsockets { get; set; } #endregion Public Properties @@ -112,49 +111,50 @@ public void SaveSettingsToFile() if (!String.IsNullOrEmpty(this.MqttPassword)) { this.EncryptedMqttPassword = CryptoHelper.EncryptString(this.MqttPassword); - }else + } + else { this.EncryptedMqttPassword = ""; } if (!String.IsNullOrEmpty(this.PlainTeamsToken)) { this.TeamsToken = CryptoHelper.EncryptString(this.PlainTeamsToken); - }else + } + else { this.TeamsToken = ""; } - if(string.IsNullOrEmpty(this.SensorPrefix)) + if (string.IsNullOrEmpty(this.SensorPrefix)) { this.SensorPrefix = System.Environment.MachineName; } // newcode - const string appName = "TEAMS2HA"; // Your application's name - string exePath = System.Windows.Forms.Application.ExecutablePath; + const string appName = "TEAMS2HA"; // Your application's name + string exePath = System.Windows.Forms.Application.ExecutablePath; - // Open the registry key for the current user's startup programs - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true)) + // Open the registry key for the current user's startup programs + using (RegistryKey key = Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Run", true)) + { + if (this.RunAtWindowsBoot) { - if (this.RunAtWindowsBoot) - { - // Set the application to start with Windows startup by adding a registry value - key.SetValue(appName, exePath); - } - else - { - // Remove the registry value to prevent the application from starting with - // Windows startup - key.DeleteValue(appName, false); - } + // Set the application to start with Windows startup by adding a registry value + key.SetValue(appName, exePath); + } + else + { + // Remove the registry value to prevent the application from starting with + // Windows startup + key.DeleteValue(appName, false); } - + } + Log.Debug("SetStartupAsync: Startup set"); // Serialize and save string json = JsonConvert.SerializeObject(this, Formatting.Indented); File.WriteAllText(_settingsFilePath, json); } - #endregion Public Methods #region Private Methods @@ -187,7 +187,6 @@ private void LoadSettingsFromFile() } } - #endregion Private Methods } @@ -195,24 +194,26 @@ public partial class MainWindow : Window { #region Private Fields + private MenuItem _aboutMenuItem; private MeetingUpdate _latestMeetingUpdate; - private AppSettings _settings; - private MenuItem _mqttStatusMenuItem; - private MenuItem _teamsStatusMenuItem; private MenuItem _logMenuItem; - private MenuItem _aboutMenuItem; + private MqttManager _mqttManager; + private MenuItem _mqttStatusMenuItem; + + //private string Mqtttopic; + private Dictionary _previousSensorStates = new Dictionary(); + + private AppSettings _settings; private string _settingsFilePath; private string _teamsApiKey; private API.WebSocketClient _teamsClient; + private MenuItem _teamsStatusMenuItem; private Action _updateTokenAction; private string deviceid; private bool isDarkTheme = false; private MqttClientWrapper mqttClientWrapper; private System.Timers.Timer mqttKeepAliveTimer; - - //private string Mqtttopic; - private Dictionary _previousSensorStates = new Dictionary(); private List sensorNames = new List { @@ -220,7 +221,7 @@ public partial class MainWindow : Window }; private bool teamspaired = false; - private MqttManager _mqttManager; + #endregion Private Fields #region Public Constructors @@ -230,12 +231,10 @@ public MainWindow() { // Get the local application data folder path var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - - // Configure logging LoggingConfig.Configure(); - + // Create the TEAMS2HA folder in the local application data folder var appDataFolder = Path.Combine(localAppData, "TEAMS2HA"); Log.Debug("Set Folder Path to {path}", appDataFolder); @@ -254,10 +253,9 @@ public MainWindow() deviceid = System.Environment.MachineName; } else - { + { deviceid = _settings.SensorPrefix; } - // Log the settings file path Log.Debug("Settings file path is {path}", _settingsFilePath); @@ -285,7 +283,7 @@ public MainWindow() ); _mqttManager = new MqttManager(mqttClientWrapper, settings, sensorNames, deviceid); _mqttManager.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; - _mqttManager.StatusUpdated += UpdateMqttStatus; + _mqttManager.StatusUpdated += UpdateMqttStatus; _mqttManager.CommandToTeams += HandleCommandToTeams; // Set the action to be performed when a new token is updated _updateTokenAction = newToken => @@ -309,10 +307,9 @@ public MainWindow() //mqttKeepAliveTimer.Elapsed += OnTimedEvent; //mqttKeepAliveTimer.AutoReset = true; //mqttKeepAliveTimer.Enabled = true; - + // Initialize the MQTT publish timer _mqttManager.InitializeMqttPublishTimer(); - } #endregion Public Constructors @@ -369,73 +366,81 @@ protected override void OnStateChanged(EventArgs e) #endregion Protected Methods #region Private Methods - private void UpdateMqttConnectionStatus(string status) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - } - private void SetWindowTitle() + private void AboutMenuItem_Click(object sender, RoutedEventArgs e) { - var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; - this.Title = $"Teams2HA - Version {version}"; + string currentTheme = _settings.Theme; // Assuming this is where the theme is stored + var aboutWindow = new AboutWindow(deviceid, MyNotifyIcon); + aboutWindow.Owner = this; + aboutWindow.ShowDialog(); } - private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) + + private void ApplyTheme(string theme) { - if (e.Mode == PowerModes.Resume) + isDarkTheme = theme == "Dark"; + Uri themeUri; + if (theme == "Dark") { - Log.Information("System is waking up from sleep. Re-establishing connections..."); - // Implement logic to re-establish connections - ReestablishConnections(); + themeUri = new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Dark.xaml"); + isDarkTheme = true; } - } - private void UpdateMqttStatus(string status) - { - Dispatcher.Invoke(() => + else { - // Assuming MQTTConnectionStatus is a Label or similar control - MQTTConnectionStatus.Text = $"MQTT Status: {status}"; - UpdateStatusMenuItems(); - }); + themeUri = new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml"); + isDarkTheme = false; + } + + // Update the theme + var existingTheme = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source == themeUri); + if (existingTheme == null) + { + existingTheme = new ResourceDictionary() { Source = themeUri }; + Application.Current.Resources.MergedDictionaries.Add(existingTheme); + } + + // Remove the other theme + var otherThemeUri = isDarkTheme + ? new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml") + : new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Dark.xaml"); + + var currentTheme = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source == otherThemeUri); + if (currentTheme != null) + { + Application.Current.Resources.MergedDictionaries.Remove(currentTheme); + } } - private void MqttManager_ConnectionStatusChanged(string status) + + private bool CheckIfMqttSettingsChanged(AppSettings newSettings) { - Dispatcher.Invoke(() => - { - MQTTConnectionStatus.Text = status; // Ensure MQTTConnectionStatus is the correct UI element's name - }); + 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.UseWebsockets != currentSettings.UseWebsockets || + newSettings.IgnoreCertificateErrors != currentSettings.IgnoreCertificateErrors; } - private async Task HandleCommandToTeams(string jsonMessage) + + private bool CheckIfSensorPrefixChanged(AppSettings newSettings) { - if (_teamsClient != null) - { - await _teamsClient.SendMessageAsync(jsonMessage); - } + var currentSettings = AppSettings.Instance; + deviceid = newSettings.SensorPrefix; + return newSettings.SensorPrefix != currentSettings.SensorPrefix; } - private async void ReestablishConnections() + + private async void CheckMqttConnection() { - try - { - if (!mqttClientWrapper.IsConnected) - { - await mqttClientWrapper.ConnectAsync(); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - await _mqttManager.SetupMqttSensors(); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - } - if (!_teamsClient.IsConnected) - { - await initializeteamsconnection(); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - } - } - catch (Exception ex) + if (mqttClientWrapper != null && !mqttClientWrapper.IsConnected && !mqttClientWrapper.IsAttemptingConnection) { - Log.Error($"Error re-establishing connections: {ex.Message}"); + Log.Debug("CheckMqttConnection: MQTT Client Not Connected. Attempting reconnection."); + await mqttClientWrapper.ConnectAsync(); + await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + UpdateConnectionStatus(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); } } - private void CreateNotifyIconContextMenu() { ContextMenu contextMenu = new ContextMenu(); @@ -474,105 +479,21 @@ private void CreateNotifyIconContextMenu() MyNotifyIcon.ContextMenu = contextMenu; } - private void ShowHideMenuItem_Click(object sender, RoutedEventArgs e) - { - if (this.IsVisible) - { - this.Hide(); - } - else - { - this.Show(); - this.WindowState = WindowState.Normal; - } - } - - private void AboutMenuItem_Click(object sender, RoutedEventArgs e) - { - string currentTheme = _settings.Theme; // Assuming this is where the theme is stored - var aboutWindow = new AboutWindow(deviceid, MyNotifyIcon); - aboutWindow.Owner = this; - aboutWindow.ShowDialog(); - } - - private void UpdateStatusMenuItems() - { - _mqttStatusMenuItem.Header = mqttClientWrapper != null && mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - _teamsStatusMenuItem.Header = _teamsClient != null && _teamsClient.IsConnected ? "Teams Status: Connected" : "Teams Status: Disconnected"; - } private void ExitMenuItem_Click(object sender, RoutedEventArgs e) { // Handle the click event for the exit menu item (Close the application) Application.Current.Shutdown(); } - private void ApplyTheme(string theme) - { - isDarkTheme = theme == "Dark"; - Uri themeUri; - if (theme == "Dark") - { - themeUri = new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Dark.xaml"); - isDarkTheme = true; - } - else - { - themeUri = new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml"); - isDarkTheme = false; - } - - // Update the theme - var existingTheme = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source == themeUri); - if (existingTheme == null) - { - existingTheme = new ResourceDictionary() { Source = themeUri }; - Application.Current.Resources.MergedDictionaries.Add(existingTheme); - } - - // Remove the other theme - var otherThemeUri = isDarkTheme - ? new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml") - : new Uri("pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Dark.xaml"); - - var currentTheme = Application.Current.Resources.MergedDictionaries.FirstOrDefault(d => d.Source == otherThemeUri); - if (currentTheme != null) - { - Application.Current.Resources.MergedDictionaries.Remove(currentTheme); - } - } - private async void CheckMqttConnection() + private async Task HandleCommandToTeams(string jsonMessage) { - if (mqttClientWrapper != null && !mqttClientWrapper.IsConnected && !mqttClientWrapper.IsAttemptingConnection) + if (_teamsClient != null) { - Log.Debug("CheckMqttConnection: MQTT Client Not Connected. Attempting reconnection."); - await mqttClientWrapper.ConnectAsync(); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - UpdateConnectionStatus(); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); + await _teamsClient.SendMessageAsync(jsonMessage); } } - private void UpdateConnectionStatus() - { - Dispatcher.Invoke(() => - { - MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - }); - } - - - - - - - - - - - - private async Task initializeteamsconnection() { string teamsToken = _settings.PlainTeamsToken; @@ -604,7 +525,7 @@ private async Task initializeteamsconnection() { Dispatcher.Invoke(() => TeamsConnectionStatus.Text = "Teams Status: Connected"); // ADD in code to set the connected status as a sensor - + State.Instance.teamsRunning = true; Log.Debug("initializeteamsconnection: WebSocketClient Connected"); } @@ -655,7 +576,6 @@ private void LogsButton_Click(object sender, RoutedEventArgs e) } } - private async void MainPage_Loaded(object sender, RoutedEventArgs e) { //LoadSettings(); @@ -669,14 +589,14 @@ private async void MainPage_Loaded(object sender, RoutedEventArgs e) MQTTPasswordBox.Password = _settings.MqttPassword; MqttAddress.Text = _settings.MqttAddress; // Added to set the sensor prefix - if(string.IsNullOrEmpty(_settings.SensorPrefix)) + if (string.IsNullOrEmpty(_settings.SensorPrefix)) { SensorPrefixBox.Text = System.Environment.MachineName; } else { SensorPrefixBox.Text = _settings.SensorPrefix; - } + } SensorPrefixBox.Text = _settings.SensorPrefix; MqttPort.Text = _settings.MqttPort; if (_settings.PlainTeamsToken == null) @@ -707,6 +627,14 @@ private void MainPage_Unloaded(object sender, RoutedEventArgs e) Log.Debug("MainPage_Unloaded: Teams Client Connection Status unsubscribed"); } + private void MqttManager_ConnectionStatusChanged(string status) + { + Dispatcher.Invoke(() => + { + MQTTConnectionStatus.Text = status; // Ensure MQTTConnectionStatus is the correct UI element's name + }); + } + private void MyNotifyIcon_Click(object sender, EventArgs e) { if (this.WindowState == WindowState.Minimized) @@ -722,7 +650,15 @@ private void MyNotifyIcon_Click(object sender, EventArgs e) } } - + private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) + { + if (e.Mode == PowerModes.Resume) + { + Log.Information("System is waking up from sleep. Re-establishing connections..."); + // Implement logic to re-establish connections + ReestablishConnections(); + } + } private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) { @@ -730,10 +666,39 @@ private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) CheckMqttConnection(); } - - - + private async void ReestablishConnections() + { + try + { + if (!mqttClientWrapper.IsConnected) + { + await mqttClientWrapper.ConnectAsync(); + await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + await _mqttManager.SetupMqttSensors(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); + } + if (!_teamsClient.IsConnected) + { + await initializeteamsconnection(); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); + } + } + catch (Exception ex) + { + Log.Error($"Error re-establishing connections: {ex.Message}"); + } + } + private async void SaveSettings_Click(object sender, RoutedEventArgs e) + { + Log.Debug("SaveSettings_Click: Save Settings Clicked" + _settings.ToString); + // uncomment below for testing ** insecure as tokens exposed in logs! ** + //foreach(var setting in _settings.GetType().GetProperties()) + //{ + // Log.Debug(setting.Name + " " + setting.GetValue(_settings)); + //} + await SaveSettingsAsync(); + } private async Task SaveSettingsAsync() { @@ -756,11 +721,9 @@ private async Task SaveSettingsAsync() settings.SensorPrefix = System.Environment.MachineName; SensorPrefixBox.Text = System.Environment.MachineName; } - else { settings.SensorPrefix = SensorPrefixBox.Text;} - - + else { settings.SensorPrefix = SensorPrefixBox.Text; } }); - + // Check if MQTT settings have changed (consider abstracting this logic into a separate method) bool mqttSettingsChanged = CheckIfMqttSettingsChanged(settings); // Check if Sensore Prefix has changed @@ -772,26 +735,30 @@ private async Task SaveSettingsAsync() await _mqttManager.ReconnectToMqttServerAsync(); await _mqttManager.SetupMqttSensors(); await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); - } - private async void SaveSettings_Click(object sender, RoutedEventArgs e) + private async Task SetStartupAsync(bool startWithWindows) { - Log.Debug("SaveSettings_Click: Save Settings Clicked" + _settings.ToString); - // uncomment below for testing ** insecure as tokens exposed in logs! ** - //foreach(var setting in _settings.GetType().GetProperties()) - //{ - // Log.Debug(setting.Name + " " + setting.GetValue(_settings)); - //} - await SaveSettingsAsync(); } - private async Task SetStartupAsync(bool startWithWindows) + private void SetWindowTitle() { - + var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + this.Title = $"Teams2HA - Version {version}"; } - + private void ShowHideMenuItem_Click(object sender, RoutedEventArgs e) + { + if (this.IsVisible) + { + this.Hide(); + } + else + { + this.Show(); + this.WindowState = WindowState.Normal; + } + } private async void TeamsClient_TeamsUpdateReceived(object sender, WebSocketClient.TeamsUpdateEventArgs e) { @@ -812,24 +779,19 @@ private void TeamsConnectionStatusChanged(bool isConnected) { TeamsConnectionStatus.Text = isConnected ? "Teams: Connected" : "Teams: Disconnected"; UpdateStatusMenuItems(); - if(isConnected == true) + if (isConnected == true) { - State.Instance.teamsRunning = true; - + State.Instance.teamsRunning = true; } else { State.Instance.teamsRunning = false; - _= _mqttManager.PublishConfigurations(null, _settings); + _ = _mqttManager.PublishConfigurations(null, _settings); } - + Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); - }); } - - - private async void TestTeamsConnection_Click(object sender, RoutedEventArgs e) { @@ -849,24 +811,6 @@ 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.UseWebsockets != currentSettings.UseWebsockets || - newSettings.IgnoreCertificateErrors != currentSettings.IgnoreCertificateErrors; - } - private bool CheckIfSensorPrefixChanged(AppSettings newSettings) - { - var currentSettings = AppSettings.Instance; - deviceid = newSettings.SensorPrefix; - return newSettings.SensorPrefix != currentSettings.SensorPrefix; - - } private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) { @@ -879,23 +823,49 @@ private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) _ = SaveSettingsAsync(); } - #endregion Private Methods + private void UpdateConnectionStatus() + { + Dispatcher.Invoke(() => + { + MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; + Dispatcher.Invoke(() => UpdateStatusMenuItems()); + }); + } - private void Websockets_Checked(object sender, RoutedEventArgs e) + private void UpdateMqttConnectionStatus(string status) { + Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); + Dispatcher.Invoke(() => UpdateStatusMenuItems()); + } - _settings.UseWebsockets = true; - // Disable the mqtt port box - // MqttPort.IsEnabled = false; + private void UpdateMqttStatus(string status) + { + Dispatcher.Invoke(() => + { + // Assuming MQTTConnectionStatus is a Label or similar control + MQTTConnectionStatus.Text = $"MQTT Status: {status}"; + UpdateStatusMenuItems(); + }); + } + + private void UpdateStatusMenuItems() + { + _mqttStatusMenuItem.Header = mqttClientWrapper != null && mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; + _teamsStatusMenuItem.Header = _teamsClient != null && _teamsClient.IsConnected ? "Teams Status: Connected" : "Teams Status: Disconnected"; + } + #endregion Private Methods + private void Websockets_Checked(object sender, RoutedEventArgs e) + { + _settings.UseWebsockets = true; + // Disable the mqtt port box MqttPort.IsEnabled = false; } + private void Websockets_Unchecked(object sender, RoutedEventArgs e) { - _settings.UseWebsockets = false; // MqttPort.IsEnabled = true; - } } } \ No newline at end of file From abb105e6383d5c0e935a41bb28b68ac4b045ceb3 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 18:10:07 +0000 Subject: [PATCH 12/31] refactored token handling for additional security --- CryptoHelper.cs | 15 +++++++++++++++ MainWindow.xaml.cs | 34 +++++++++++++++++++++++----------- TEAMS2HA.csproj | 4 ++-- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/CryptoHelper.cs b/CryptoHelper.cs index 55bee30..99de922 100644 --- a/CryptoHelper.cs +++ b/CryptoHelper.cs @@ -9,6 +9,13 @@ public static class CryptoHelper { public static string EncryptString(string plainText) { + // Check if the input string is null or empty + if (string.IsNullOrEmpty(plainText)) + { + // Return null or throw an exception as per your application's error handling policy + return null; // Or throw new ArgumentNullException(nameof(plainText)); + } + byte[] plainTextBytes = Encoding.UTF8.GetBytes(plainText); byte[] encryptedBytes = ProtectedData.Protect(plainTextBytes, null, DataProtectionScope.CurrentUser); @@ -17,10 +24,18 @@ public static string EncryptString(string plainText) public static string DecryptString(string encryptedText) { + // Check if the input string is null or empty + if (string.IsNullOrEmpty(encryptedText)) + { + // Return null or throw an exception as per your application's error handling policy + return null; // Or throw new ArgumentNullException(nameof(encryptedText)); + } + byte[] encryptedBytes = Convert.FromBase64String(encryptedText); byte[] decryptedBytes = ProtectedData.Unprotect(encryptedBytes, null, DataProtectionScope.CurrentUser); return Encoding.UTF8.GetString(decryptedBytes); } } + } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 7e36000..3387b95 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -29,7 +29,8 @@ public class AppSettings private static readonly object _lock = new object(); private static readonly string _settingsFilePath; - + private string _mqttPassword; // Store the encrypted version internally + private string _teamsToken; // Store the encrypted version internally // Static variable for the singleton instance private static AppSettings _instance; @@ -77,14 +78,26 @@ public static AppSettings Instance } // Properties - public string EncryptedMqttPassword { get; set; } - + public string EncryptedMqttPassword + { + get => _mqttPassword; + set => _mqttPassword = value; // Only for deserialization + } + public string EncryptedTeamsToken + { + get => _teamsToken; + set => _teamsToken = value; // Only for deserialization + } public bool IgnoreCertificateErrors { get; set; } public string MqttAddress { get; set; } [JsonIgnore] - public string MqttPassword { get; set; } + public string MqttPassword + { + get => CryptoHelper.DecryptString(_mqttPassword); + set => _mqttPassword = CryptoHelper.EncryptString(value); + } public string MqttPort { get; set; } public string MqttUsername { get; set; } @@ -95,7 +108,12 @@ public static AppSettings Instance public bool RunAtWindowsBoot { get; set; } public bool RunMinimized { get; set; } public string SensorPrefix { get; set; } - public string TeamsToken { get; set; } + [JsonIgnore] + public string TeamsToken + { + get => CryptoHelper.DecryptString(_teamsToken); + set => _teamsToken = CryptoHelper.EncryptString(value); + } public string Theme { get; set; } public bool UseTLS { get; set; } public bool UseWebsockets { get; set; } @@ -213,7 +231,6 @@ public partial class MainWindow : Window private string deviceid; private bool isDarkTheme = false; private MqttClientWrapper mqttClientWrapper; - private System.Timers.Timer mqttKeepAliveTimer; private List sensorNames = new List { @@ -302,11 +319,6 @@ public MainWindow() _previousSensorStates[$"{deviceid}_{sensor}"] = ""; } - // Create a timer for MQTT keep alive - //mqttKeepAliveTimer = new System.Timers.Timer(60000); // Set interval to 60 seconds (60000 ms) - //mqttKeepAliveTimer.Elapsed += OnTimedEvent; - //mqttKeepAliveTimer.AutoReset = true; - //mqttKeepAliveTimer.Enabled = true; // Initialize the MQTT publish timer _mqttManager.InitializeMqttPublishTimer(); diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index c893f0f..44e26e4 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.391 - 1.1.0.391 + 1.1.0.394 + 1.1.0.394 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From b7abd373585f057025054fb1b865a97a23a49693 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 18:12:36 +0000 Subject: [PATCH 13/31] refactored CheckMqttConnection to use UpdateStatusMenuItems() rather than updating the UI directly --- MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 3387b95..286ab47 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -449,7 +449,7 @@ private async void CheckMqttConnection() await mqttClientWrapper.ConnectAsync(); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); UpdateConnectionStatus(); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); + UpdateStatusMenuItems(); } } From c769c6d4cd4532fcb00b1376cf0bf2ec950c5d7f Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Sun, 10 Mar 2024 18:22:18 +0000 Subject: [PATCH 14/31] added connectinhg.. status updates for connecting attempts --- API/MqttClientWrapper.cs | 2 ++ API/MqttManager.cs | 1 + MainWindow.xaml.cs | 51 +++++++++++++++++++++++++++------------- TEAMS2HA.csproj | 4 ++-- 4 files changed, 40 insertions(+), 18 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index 2003eb4..bc93f74 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -18,6 +18,7 @@ namespace TEAMS2HA.API public class MqttClientWrapper { #region Private Fields + public event Action ConnectionAttempting; private const int MaxConnectionRetries = 2; private const int RetryDelayMilliseconds = 1000; @@ -183,6 +184,7 @@ public async Task ConnectAsync() } _isAttemptingConnection = true; + ConnectionAttempting?.Invoke("MQTT Status: Connecting..."); int retryCount = 0; while (retryCount < MaxConnectionRetries && !_mqttClient.IsConnected) diff --git a/API/MqttManager.cs b/API/MqttManager.cs index 79e1919..4a834d2 100644 --- a/API/MqttManager.cs +++ b/API/MqttManager.cs @@ -22,6 +22,7 @@ public class MqttManager private MqttClientWrapper _mqttClientWrapper; private Dictionary _previousSensorStates; private System.Timers.Timer mqttPublishTimer; + public delegate Task CommandToTeamsHandler(string jsonMessage); diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 286ab47..66aab3f 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -302,6 +302,8 @@ public MainWindow() _mqttManager.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; _mqttManager.StatusUpdated += UpdateMqttStatus; _mqttManager.CommandToTeams += HandleCommandToTeams; + mqttClientWrapper.ConnectionAttempting += MqttManager_ConnectionAttempting; + // Set the action to be performed when a new token is updated _updateTokenAction = newToken => { @@ -421,6 +423,15 @@ private void ApplyTheme(string theme) Application.Current.Resources.MergedDictionaries.Remove(currentTheme); } } + private void MqttManager_ConnectionAttempting(string status) + { + Dispatcher.Invoke(() => + { + MQTTConnectionStatus.Text = status; + _mqttStatusMenuItem.Header = status; // Update the system tray menu item as well + // No need to update other status menu items as this is specifically for MQTT connection + }); + } private bool CheckIfMqttSettingsChanged(AppSettings newSettings) { @@ -448,7 +459,7 @@ private async void CheckMqttConnection() Log.Debug("CheckMqttConnection: MQTT Client Not Connected. Attempting reconnection."); await mqttClientWrapper.ConnectAsync(); await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - UpdateConnectionStatus(); + //UpdateConnectionStatus(); UpdateStatusMenuItems(); } } @@ -835,20 +846,20 @@ private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) _ = SaveSettingsAsync(); } - private void UpdateConnectionStatus() - { - Dispatcher.Invoke(() => - { - MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - }); - } + //private void UpdateConnectionStatus() + //{ + // Dispatcher.Invoke(() => + // { + // MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; + // Dispatcher.Invoke(() => UpdateStatusMenuItems()); + // }); + //} - private void UpdateMqttConnectionStatus(string status) - { - Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); - Dispatcher.Invoke(() => UpdateStatusMenuItems()); - } + //private void UpdateMqttConnectionStatus(string status) + //{ + // Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); + // Dispatcher.Invoke(() => UpdateStatusMenuItems()); + //} private void UpdateMqttStatus(string status) { @@ -862,10 +873,18 @@ private void UpdateMqttStatus(string status) private void UpdateStatusMenuItems() { - _mqttStatusMenuItem.Header = mqttClientWrapper != null && mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - _teamsStatusMenuItem.Header = _teamsClient != null && _teamsClient.IsConnected ? "Teams Status: Connected" : "Teams Status: Disconnected"; + Dispatcher.Invoke(() => + { + // Update MQTT connection status text + MQTTConnectionStatus.Text = mqttClientWrapper != null && mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; + // Update menu items + _mqttStatusMenuItem.Header = MQTTConnectionStatus.Text; // Reuse the text set above + _teamsStatusMenuItem.Header = _teamsClient != null && _teamsClient.IsConnected ? "Teams Status: Connected" : "Teams Status: Disconnected"; + // Add other status updates here as necessary + }); } + #endregion Private Methods private void Websockets_Checked(object sender, RoutedEventArgs e) diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 44e26e4..161835d 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.394 - 1.1.0.394 + 1.1.0.396 + 1.1.0.396 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From da63391218b7ca0c7267b789f9344fd9e06c01bb Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Mon, 11 Mar 2024 10:34:06 +0000 Subject: [PATCH 15/31] tidy up sensors, added code to gracefullly set sensors to off when exiting --- API/MqttClientWrapper.cs | 10 +++++----- API/MqttManager.cs | 22 +++++++++++----------- MainWindow.xaml.cs | 33 +++++++++++++++++++++++++++++++-- TEAMS2HA.csproj | 4 ++-- 4 files changed, 49 insertions(+), 20 deletions(-) diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs index bc93f74..983512b 100644 --- a/API/MqttClientWrapper.cs +++ b/API/MqttClientWrapper.cs @@ -163,12 +163,12 @@ public static List GetEntityNames(string deviceId) $"switch.{deviceId}_ismuted", $"switch.{deviceId}_isvideoon", $"switch.{deviceId}_ishandraised", - $"sensor.{deviceId}_isrecordingon", - $"sensor.{deviceId}_isinmeeting", - $"sensor.{deviceId}_issharing", - $"sensor.{deviceId}_hasunreadmessages", + $"binary_sensor.{deviceId}_isrecordingon", + $"binary_sensor.{deviceId}_isinmeeting", + $"binary_sensor.{deviceId}_issharing", + $"binary_sensor.{deviceId}_hasunreadmessages", $"switch.{deviceId}_isbackgroundblurred", - $"sensor.{deviceId}_teamsRunning" + $"binary_sensor.{deviceId}_teamsRunning" }; return entityNames; diff --git a/API/MqttManager.cs b/API/MqttManager.cs index 4a834d2..57bf7a2 100644 --- a/API/MqttManager.cs +++ b/API/MqttManager.cs @@ -157,14 +157,14 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings } }; } - foreach (var sensor in _sensorNames) + foreach (var binary_sensor in _sensorNames) { - string sensorKey = $"{_deviceId}_{sensor}"; - string sensorName = $"{sensor}".ToLower().Replace(" ", "_"); - string deviceClass = DetermineDeviceClass(sensor); - string icon = DetermineIcon(sensor, meetingUpdate.MeetingState); - string stateValue = GetStateValue(sensor, meetingUpdate); - string uniqueId = $"{_deviceId}_{sensor}"; + string sensorKey = $"{_deviceId}_{binary_sensor}"; + string sensorName = $"{binary_sensor}".ToLower().Replace(" ", "_"); + string deviceClass = DetermineDeviceClass(binary_sensor); + string icon = DetermineIcon(binary_sensor, meetingUpdate.MeetingState); + string stateValue = GetStateValue(binary_sensor, meetingUpdate); + string uniqueId = $"{_deviceId}_{binary_sensor}"; if (!_previousSensorStates.TryGetValue(sensorKey, out var previousState) || previousState != stateValue) { @@ -193,17 +193,17 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings } else { - Log.Debug($"Switch configuration JSON is null or empty for sensor: {sensor}"); + Log.Debug($"Switch configuration JSON is null or empty for sensor: {binary_sensor}"); } } else { - Log.Debug($"configTopic or switchConfig is null for sensor: {sensor}"); + Log.Debug($"configTopic or switchConfig is null for sensor: {binary_sensor}"); } await _mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(switchConfig), true); await _mqttClientWrapper.PublishAsync(switchConfig.state_topic, stateValue); } - else if (deviceClass == "sensor") + else if (deviceClass == "binary_sensor") { var binarySensorConfig = new { @@ -363,7 +363,7 @@ private string DetermineDeviceClass(string sensor) case "IsRecordingOn": case "IsSharing": case "teamsRunning": - return "sensor"; // These are true/false sensors + return "binary_sensor"; // These are true/false sensors default: return null; // Or a default device class if appropriate } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 66aab3f..798e453 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -343,8 +343,35 @@ public async Task InitializeConnections() #region Protected Methods - protected override void OnClosing(CancelEventArgs e) + protected override async void OnClosing(CancelEventArgs e) { + // Publish and update to set all binary_sensors and switches to "OFF" + foreach (var sensorName in sensorNames) + { + // Format the topics correctly without the device name prefix + string topicState = $"homeassistant/binary_sensor/{sensorName.ToLower()}/state"; + string topicSwitch = $"homeassistant/switch/{sensorName.ToLower()}/state"; + + // Assuming the "OFF" state is correct for your devices, change as needed. + try + { + await mqttClientWrapper.PublishAsync(topicState, "OFF", retain: true); + await mqttClientWrapper.PublishAsync(topicSwitch, "OFF", retain: true); + } + catch (Exception ex) + { + // Log or handle any exceptions thrown during publishing + Log.Error($"Failed to publish 'off' state for {sensorName}: {ex.Message}"); + } + } + + // Unsubscribe from events and clean up + if (_mqttManager != null) + { + _mqttManager.ConnectionStatusChanged -= MqttManager_ConnectionStatusChanged; + _mqttManager.StatusUpdated -= UpdateMqttStatus; + _mqttManager.CommandToTeams -= HandleCommandToTeams; + } if (_teamsClient != null) { _teamsClient.TeamsUpdateReceived -= TeamsClient_TeamsUpdateReceived; @@ -352,14 +379,16 @@ protected override void OnClosing(CancelEventArgs e) } if (mqttClientWrapper != null) { + await mqttClientWrapper.DisconnectAsync(); // Properly disconnect before disposing mqttClientWrapper.Dispose(); Log.Debug("MQTT Client Disposed"); } MyNotifyIcon.Dispose(); - base.OnClosing(e); + base.OnClosing(e); // Call the base class method SystemEvents.PowerModeChanged -= OnPowerModeChanged; } + protected override void OnStateChanged(EventArgs e) { if (WindowState == WindowState.Minimized) diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 161835d..8d0dcfc 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.396 - 1.1.0.396 + 1.1.0.402 + 1.1.0.402 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 69a25a22305ea132814775f33556f41efd7d8a56 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Mon, 11 Mar 2024 10:45:08 +0000 Subject: [PATCH 16/31] onclosing events --- MainWindow.xaml.cs | 22 +++------------------- TEAMS2HA.csproj | 4 ++-- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 798e453..92d4921 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -345,25 +345,7 @@ public async Task InitializeConnections() protected override async void OnClosing(CancelEventArgs e) { - // Publish and update to set all binary_sensors and switches to "OFF" - foreach (var sensorName in sensorNames) - { - // Format the topics correctly without the device name prefix - string topicState = $"homeassistant/binary_sensor/{sensorName.ToLower()}/state"; - string topicSwitch = $"homeassistant/switch/{sensorName.ToLower()}/state"; - - // Assuming the "OFF" state is correct for your devices, change as needed. - try - { - await mqttClientWrapper.PublishAsync(topicState, "OFF", retain: true); - await mqttClientWrapper.PublishAsync(topicSwitch, "OFF", retain: true); - } - catch (Exception ex) - { - // Log or handle any exceptions thrown during publishing - Log.Error($"Failed to publish 'off' state for {sensorName}: {ex.Message}"); - } - } + // Unsubscribe from events and clean up if (_mqttManager != null) @@ -377,6 +359,8 @@ protected override async void OnClosing(CancelEventArgs e) _teamsClient.TeamsUpdateReceived -= TeamsClient_TeamsUpdateReceived; Log.Debug("Teams Client Disconnected"); } + // we want all the sensors to be off if we are exiting, lets initialise them, to do this + await _mqttManager.SetupMqttSensors(); if (mqttClientWrapper != null) { await mqttClientWrapper.DisconnectAsync(); // Properly disconnect before disposing diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 8d0dcfc..f54b63b 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.402 - 1.1.0.402 + 1.1.0.405 + 1.1.0.405 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 0315d423543a8cbeddca343b57e20a1c00d76b62 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Mon, 11 Mar 2024 15:34:02 +0000 Subject: [PATCH 17/31] Major update to MQTT and refactored into mqttservice --- API/MqttClientWrapper.cs | 377 --------------------- API/MqttManager.cs | 484 --------------------------- API/MqttService.cs | 692 +++++++++++++++++++++++++++++++++++++++ AboutWindow.xaml.cs | 5 +- MainWindow.xaml.cs | 103 +++--- TEAMS2HA.csproj | 4 +- 6 files changed, 737 insertions(+), 928 deletions(-) delete mode 100644 API/MqttClientWrapper.cs delete mode 100644 API/MqttManager.cs create mode 100644 API/MqttService.cs diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs deleted file mode 100644 index 983512b..0000000 --- a/API/MqttClientWrapper.cs +++ /dev/null @@ -1,377 +0,0 @@ -using MQTTnet; -using MQTTnet.Client; -using MQTTnet.Protocol; -using Serilog; -using System; -using System.Security.Cryptography.X509Certificates; -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 -{ - public class MqttClientWrapper - { - #region Private Fields - public event Action ConnectionAttempting; - - private const int MaxConnectionRetries = 2; - private const int RetryDelayMilliseconds = 1000; - private bool _isAttemptingConnection = false; - private MqttClient _mqttClient; - private MqttClientOptions _mqttOptions; - //wait a couple of seconds before retrying a connection attempt - - #endregion Private Fields - - #region Public Events - - public event Action ConnectionStatusChanged; - - #endregion Public Events - - #region Public Constructors - - [Obsolete] - public MqttClientWrapper(string clientId, string mqttBroker, string mqttPort, string username, string password, bool useTLS, bool ignoreCertificateErrors, bool useWebsockets) - { - try - { - var factory = new MqttFactory(); - _mqttClient = (MqttClient?)factory.CreateMqttClient(); - - if (!int.TryParse(mqttPort, out int mqttportInt)) - { - mqttportInt = 1883; // Default MQTT port - Log.Warning($"Invalid MQTT port provided, defaulting to {mqttportInt}"); - } - - var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() - .WithClientId(clientId) - .WithCredentials(username, password) - .WithCleanSession() - .WithTimeout(TimeSpan.FromSeconds(5)); - - string protocol = useWebsockets ? "ws" : "tcp"; - string connectionType = useTLS ? "with TLS" : "without TLS"; - - if (useWebsockets) - { - string websocketUri = useTLS ? $"wss://{mqttBroker}:{mqttportInt}" : $"ws://{mqttBroker}:{mqttportInt}"; - mqttClientOptionsBuilder.WithWebSocketServer(websocketUri); - Log.Information($"Configuring MQTT client for WebSocket {connectionType} connection to {websocketUri}"); - } - else - { - mqttClientOptionsBuilder.WithTcpServer(mqttBroker, mqttportInt); - Log.Information($"Configuring MQTT client for TCP {connectionType} connection to {mqttBroker}:{mqttportInt}"); - } - - if (useTLS) - { - mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters - { - UseTls = true, - AllowUntrustedCertificates = ignoreCertificateErrors, - IgnoreCertificateChainErrors = ignoreCertificateErrors, - IgnoreCertificateRevocationErrors = ignoreCertificateErrors, - CertificateValidationHandler = context => - { - // Log the certificate subject - Log.Debug("Certificate Subject: {0}", context.Certificate.Subject); - - // This assumes you are trying to inspect the certificate directly; - // MQTTnet may not provide a direct IsValid flag or ChainErrors like - // .NET's X509Chain. Instead, you handle validation and log details manually: - - bool isValid = true; // You should define the logic to set this based on your validation requirements - - // Check for specific conditions, if necessary, such as expiry, issuer, etc. - // For example, if you want to ensure the certificate is issued by a specific entity: - //if (context.Certificate.Issuer != "CN=R3, O=Let's Encrypt, C=US") - //{ - // Log.Debug("Unexpected certificate issuer: {0}", context.Certificate.Issuer); - // isValid = false; // Set to false if the issuer is not the expected one - //} - - // Log any errors from the SSL policy errors if they exist - if (context.SslPolicyErrors != System.Net.Security.SslPolicyErrors.None) - { - Log.Debug("SSL policy errors: {0}", context.SslPolicyErrors.ToString()); - isValid = false; // Consider invalid if there are any SSL policy errors - } - - // You can decide to ignore certain errors by setting isValid to true - // regardless of the checks, but be careful as this might introduce - // security vulnerabilities. - if (ignoreCertificateErrors) - { - isValid = true; // Ignore certificate errors if your settings dictate - } - - return isValid; // Return the result of your checks - } - }); - } - - _mqttOptions = mqttClientOptionsBuilder.Build(); - if (_mqttClient != null) - { - _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; - } - } - catch (Exception ex) - { - Log.Error(ex, "Failed to initialize MqttClientWrapper"); - throw; // Rethrowing the exception to handle it outside or log it as fatal depending on your error handling strategy. - } - } - - public bool IsAttemptingConnection - { - get { return _isAttemptingConnection; } - private set { _isAttemptingConnection = value; } - } - - #endregion Public Constructors - - - - #region Public Events - - public event Func MessageReceived; - - #endregion Public Events - - #region Public Properties - - public bool IsConnected => _mqttClient.IsConnected; - - #endregion Public Properties - - #region Public Methods - - public static List GetEntityNames(string deviceId) - { - var entityNames = new List - { - $"switch.{deviceId}_ismuted", - $"switch.{deviceId}_isvideoon", - $"switch.{deviceId}_ishandraised", - $"binary_sensor.{deviceId}_isrecordingon", - $"binary_sensor.{deviceId}_isinmeeting", - $"binary_sensor.{deviceId}_issharing", - $"binary_sensor.{deviceId}_hasunreadmessages", - $"switch.{deviceId}_isbackgroundblurred", - $"binary_sensor.{deviceId}_teamsRunning" - }; - - return entityNames; - } - - public async Task ConnectAsync() - { - if (_mqttClient.IsConnected || _isAttemptingConnection) - { - Log.Information("MQTT client is already connected or connection attempt is in progress."); - - return; - } - - _isAttemptingConnection = true; - ConnectionAttempting?.Invoke("MQTT Status: Connecting..."); - int retryCount = 0; - - while (retryCount < MaxConnectionRetries && !_mqttClient.IsConnected) - { - try - { - 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); - } - } - - _isAttemptingConnection = false; - if (!_mqttClient.IsConnected) - { - ConnectionStatusChanged?.Invoke("MQTT Status: Disconnected (Failed to connect)"); - Log.Error("Failed to connect to MQTT broker after several attempts."); - } - } - - public async Task DisconnectAsync() - { - if (!_mqttClient.IsConnected) - { - Log.Debug("MQTTClient is not connected"); - ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); - return; - } - - try - { - await _mqttClient.DisconnectAsync(); - Log.Information("MQTT Disconnected"); - ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); - } - catch (Exception ex) - { - Debug.WriteLine($"Failed to disconnect from MQTT broker: {ex.Message}"); - } - } - - public void Dispose() - { - if (_mqttClient != null) - { - _ = _mqttClient.DisconnectAsync(); // Disconnect asynchronously - _mqttClient.Dispose(); - Log.Information("MQTT Client Disposed"); - } - } - - public async Task PublishAsync(string topic, string payload, bool retain = true) - { - try - { - // Log the topic, payload, and retain flag - Log.Information($"Publishing to topic: {topic}"); - Log.Information($"Payload: {payload}"); - Log.Information($"Retain flag: {retain}"); - - // Build the MQTT message - var message = new MqttApplicationMessageBuilder() - .WithTopic(topic) - .WithPayload(payload) - .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce) - .WithRetainFlag(retain) - .Build(); - - // Publish the message using the MQTT client - await _mqttClient.PublishAsync(message); - Log.Information("Publish successful."); - } - catch (Exception ex) - { - // Log any errors that occur during MQTT publish - Log.Information($"Error during MQTT publish: {ex.Message}"); - // Depending on the severity, you might want to rethrow the exception or handle it here. - } - } - - public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) - { - var subscribeOptions = new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) - .Build(); - try - { - await _mqttClient.SubscribeAsync(subscribeOptions); - } - catch (Exception ex) - { - Log.Information($"Error during MQTT subscribe: {ex.Message}"); - // Depending on the severity, you might want to rethrow the exception or handle it here. - } - Log.Information("Subscribing." + subscribeOptions); - } - - public void UpdateClientSettings(string mqttBroker, string mqttPort, string username, string password, bool useTLS, bool ignoreCertificateErrors, bool useWebsockets) - { - // Convert the MQTT port from string to integer, defaulting to 1883 if conversion fails - if (!int.TryParse(mqttPort, out int portNumber)) - { - portNumber = 1883; // Default MQTT port - Log.Warning($"Invalid MQTT port provided, defaulting to {portNumber}"); - } - - // Start building the new MQTT client options - var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() - .WithClientId(Guid.NewGuid().ToString()) // Use a new client ID for a new connection - .WithCredentials(username, password) - .WithCleanSession() - .WithTimeout(TimeSpan.FromSeconds(5)); - - // Setup connection type: WebSockets or TCP - if (useWebsockets) - { - string websocketUri = useTLS ? $"wss://{mqttBroker}:{portNumber}" : $"ws://{mqttBroker}:{portNumber}"; - mqttClientOptionsBuilder.WithWebSocketServer(websocketUri); - Log.Information($"Updating MQTT client settings for WebSocket {(useTLS ? "with TLS" : "without TLS")} connection to {websocketUri}"); - } - else - { - mqttClientOptionsBuilder.WithTcpServer(mqttBroker, portNumber); - Log.Information($"Updating MQTT client settings for TCP {(useTLS ? "with TLS" : "without TLS")} connection to {mqttBroker}:{portNumber}"); - } - - // Setup TLS/SSL settings if needed - if (useTLS) - { - mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters - { - UseTls = true, - // need to set the timeout for connections - - AllowUntrustedCertificates = ignoreCertificateErrors, - IgnoreCertificateChainErrors = ignoreCertificateErrors, - IgnoreCertificateRevocationErrors = ignoreCertificateErrors, - CertificateValidationHandler = context => - { - // Implement your TLS validation logic here Log any details necessary and - // return true if validation is successful For simplicity and security - // example, this will return true if ignoreCertificateErrors is true - return ignoreCertificateErrors; // WARNING: Setting this to always 'true' might pose a security risk - } - }); - } - - // Apply the new settings to the MQTT client - _mqttOptions = mqttClientOptionsBuilder.Build(); - - // If needed, log the new settings or perform any other necessary actions here - Log.Information("MQTT client settings updated successfully."); - } - - #endregion Public Methods - - #region Private Methods - - private async Task HandleReceivedApplicationMessage(MqttApplicationMessageReceivedEventArgs e) - { - if (MessageReceived != null) - { - await MessageReceived(e); - Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); - } - } - - private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) - { - Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); - // Trigger the event to notify subscribers - MessageReceived?.Invoke(e); - - return Task.CompletedTask; - } - - #endregion Private Methods - } -} \ No newline at end of file diff --git a/API/MqttManager.cs b/API/MqttManager.cs deleted file mode 100644 index 57bf7a2..0000000 --- a/API/MqttManager.cs +++ /dev/null @@ -1,484 +0,0 @@ -using MQTTnet.Client; -using MQTTnet.Protocol; -using System; -using System.Collections.Generic; -using System.Linq; -using Serilog; -using System.Text; -using System.Threading.Tasks; -using System.Windows.Threading; -using Newtonsoft.Json; -using System.Timers; - -namespace TEAMS2HA.API -{ - public class MqttManager - { - #region Private Fields - - private readonly string _deviceId; - private readonly List _sensorNames; - private readonly AppSettings _settings; - private MqttClientWrapper _mqttClientWrapper; - private Dictionary _previousSensorStates; - private System.Timers.Timer mqttPublishTimer; - - - public delegate Task CommandToTeamsHandler(string jsonMessage); - - public event CommandToTeamsHandler CommandToTeams; - - public event Action StatusUpdated; - - #endregion Private Fields - - #region Public Constructors - - public MqttManager(MqttClientWrapper mqttClientWrapper, AppSettings settings, List sensorNames, string deviceId) - { - _mqttClientWrapper = mqttClientWrapper; - _settings = settings; - _sensorNames = sensorNames; - _deviceId = deviceId; - _previousSensorStates = new Dictionary(); - InitializeConnection(); - InitializeMqttPublishTimer(); - } - - #endregion Public Constructors - - #region Public Delegates - - public delegate void ConnectionStatusChangedHandler(string status); - - #endregion Public Delegates - - #region Public Events - - public event ConnectionStatusChangedHandler ConnectionStatusChanged; - - #endregion Public Events - - #region Public Methods - - public async Task HandleIncomingCommand(MqttApplicationMessageReceivedEventArgs e) - { - string topic = e.ApplicationMessage.Topic; - Log.Debug("HandleIncomingCommand: MQTT Topic {topic}", topic); - // 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.PayloadSegment); - // Parse and handle the command - HandleSwitchCommand(topic, command); - } - } - - public async Task InitializeConnection() - { - if (_mqttClientWrapper == null) - { - UpdateConnectionStatus("MQTT Client Not Initialized"); - Log.Debug("MQTT Client Not Initialized"); - return; - } - //check we have at least an mqtt server address - if (string.IsNullOrEmpty(_settings.MqttAddress)) - { - UpdateConnectionStatus("MQTT Server Address Not Set"); - Log.Debug("MQTT Server Address Not Set"); - return; - } - int retryCount = 0; - const int maxRetries = 5; - - while (retryCount < maxRetries && !_mqttClientWrapper.IsConnected) - { - try - { - await _mqttClientWrapper.ConnectAsync(); - // Dispatcher.Invoke(() => MQTTConnectionStatus.Text = "MQTT Status: Connected"); - await _mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - - _mqttClientWrapper.MessageReceived += HandleIncomingCommand; - if (_mqttClientWrapper.IsConnected) - { - UpdateConnectionStatus("MQTT Status: Connected"); - Log.Debug("MQTT Client Connected in InitializeMQTTConnection"); - await SetupMqttSensors(); - } - return; // Exit the method if connected - } - catch (Exception ex) - { - UpdateConnectionStatus($"MQTT Status: Disconnected (Retry {retryCount + 1})"); - - Log.Debug("MQTT Retrty Count {count} {message}", retryCount, ex.Message); - retryCount++; - await Task.Delay(2000); // Wait for 2 seconds before retrying - } - } - - UpdateConnectionStatus("MQTT Status: Disconnected (Failed to connect)"); - Log.Debug("MQTT Client Failed to Connect"); - } - - public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings settings) - { - if (_mqttClientWrapper == null) - { - Log.Debug("MQTT Client Wrapper is not initialized."); - return; - } - // Define common device information for all entities. - var deviceInfo = new - { - ids = new[] { "teams2ha_" + _deviceId }, // Unique device identifier - mf = "Jimmy White", // Manufacturer name - mdl = "Teams2HA Device", // Model - name = _deviceId, // Device name - sw = "v1.0" // Software version - }; - if (meetingUpdate == null) - { - meetingUpdate = new MeetingUpdate - { - MeetingState = new MeetingState - { - IsMuted = false, - IsVideoOn = false, - IsHandRaised = false, - IsInMeeting = false, - IsRecordingOn = false, - IsBackgroundBlurred = false, - IsSharing = false, - HasUnreadMessages = false, - teamsRunning = false - } - }; - } - foreach (var binary_sensor in _sensorNames) - { - string sensorKey = $"{_deviceId}_{binary_sensor}"; - string sensorName = $"{binary_sensor}".ToLower().Replace(" ", "_"); - string deviceClass = DetermineDeviceClass(binary_sensor); - string icon = DetermineIcon(binary_sensor, meetingUpdate.MeetingState); - string stateValue = GetStateValue(binary_sensor, meetingUpdate); - string uniqueId = $"{_deviceId}_{binary_sensor}"; - - if (!_previousSensorStates.TryGetValue(sensorKey, out var previousState) || previousState != stateValue) - { - _previousSensorStates[sensorKey] = stateValue; // Update the stored state - - if (deviceClass == "switch") - { - var switchConfig = new - { - name = sensorName, - unique_id = uniqueId, - device = deviceInfo, - icon = icon, - command_topic = $"homeassistant/switch/{sensorName}/set", - state_topic = $"homeassistant/switch/{sensorName}/state", - payload_on = "ON", - payload_off = "OFF" - }; - string configTopic = $"homeassistant/switch/{sensorName}/config"; - if (!string.IsNullOrEmpty(configTopic) && switchConfig != null) - { - string switchConfigJson = JsonConvert.SerializeObject(switchConfig); - if (!string.IsNullOrEmpty(switchConfigJson)) - { - await _mqttClientWrapper.PublishAsync(configTopic, switchConfigJson, true); - } - else - { - Log.Debug($"Switch configuration JSON is null or empty for sensor: {binary_sensor}"); - } - } - else - { - Log.Debug($"configTopic or switchConfig is null for sensor: {binary_sensor}"); - } - await _mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(switchConfig), true); - await _mqttClientWrapper.PublishAsync(switchConfig.state_topic, stateValue); - } - else if (deviceClass == "binary_sensor") - { - var binarySensorConfig = new - { - name = sensorName, - unique_id = uniqueId, - device = deviceInfo, - icon = icon, - state_topic = $"homeassistant/binary_sensor/{sensorName}/state", - payload_on = "true", // Assuming "True" states map to "ON" - payload_off = "false" // Assuming "False" states map to "OFF" - }; - //string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; - //await mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); - //await mqttClientWrapper.PublishAsync(binarySensorConfig.state_topic, stateValue); - string configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; - await _mqttClientWrapper.PublishAsync(configTopic, JsonConvert.SerializeObject(binarySensorConfig), true); - - // Here's the important part: publish the initial state for each sensor - string stateTopic = $"homeassistant/binary_sensor/{sensorName}/state"; - await _mqttClientWrapper.PublishAsync(stateTopic, stateValue.ToLowerInvariant()); // Convert "True"/"False" to "on"/"off" or keep "ON"/"OFF" - } - } - } - } - - public async Task ReconnectToMqttServerAsync() - { - _mqttClientWrapper.UpdateClientSettings( - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword, - _settings.UseTLS, - _settings.IgnoreCertificateErrors, - _settings.UseWebsockets); - - // Ensure disconnection from the current MQTT server, if connected - if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) - { - await _mqttClientWrapper.DisconnectAsync(); - } - - // Attempt to connect to the MQTT server with new settings - await _mqttClientWrapper.ConnectAsync(); // Connect without checking in 'if' - - // Now, check if the connection was successful - if (_mqttClientWrapper.IsConnected) // Assuming IsConnected is a boolean property - { - StatusUpdated?.Invoke("Connected"); - await _mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - } - else - { - StatusUpdated?.Invoke("Disconnected"); - } - } - - public async Task SetupMqttSensors() - { - // Create a dummy MeetingUpdate with default values - var dummyMeetingUpdate = new MeetingUpdate - { - MeetingState = new MeetingState - { - IsMuted = false, - IsVideoOn = false, - IsHandRaised = false, - IsInMeeting = false, - IsRecordingOn = false, - IsBackgroundBlurred = false, - IsSharing = false, - HasUnreadMessages = false, - teamsRunning = false - } - }; - - // Call PublishConfigurations with the dummy MeetingUpdate - await PublishConfigurations(dummyMeetingUpdate, _settings); - } - - public void UpdateConnectionStatus(string status) - { - OnConnectionStatusChanged(status); - } - - private async void HandleSwitchCommand(string topic, string command) - { - // Determine which switch is being controlled based on the topic - string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" - int underscoreIndex = switchName.IndexOf('_'); - if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) - { - switchName = switchName.Substring(underscoreIndex + 1); - } - string jsonMessage = ""; - switch (switchName) - { - case "ismuted": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "isvideoon": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "isbackgroundblurred": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - case "ishandraised": - jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; - break; - - // Add other cases as needed - } - - if (!string.IsNullOrEmpty(jsonMessage)) - { - // Raise the event - CommandToTeams?.Invoke(jsonMessage); - } - } - - #endregion Public Methods - - #region Protected Methods - - protected virtual void OnConnectionStatusChanged(string status) - { - ConnectionStatusChanged?.Invoke(status); - } - - #endregion Protected Methods - - #region Private Methods - - public void InitializeMqttPublishTimer() - { - mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds - mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; - mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses - mqttPublishTimer.Enabled = true; // Enable the timer - Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); - } - - private string DetermineDeviceClass(string sensor) - { - switch (sensor) - { - case "IsMuted": - case "IsVideoOn": - case "IsHandRaised": - case "IsBackgroundBlurred": - return "switch"; // These are ON/OFF switches - case "IsInMeeting": - case "HasUnreadMessages": - case "IsRecordingOn": - case "IsSharing": - case "teamsRunning": - return "binary_sensor"; // These are true/false sensors - default: - return null; // Or a default device class if appropriate - } - } - - // This method determines the appropriate icon based on the sensor and meeting state - private string DetermineIcon(string sensor, MeetingState state) - { - return sensor switch - { - // If the sensor is "IsMuted", return "mdi:microphone-off" if state.IsMuted is true, - // otherwise return "mdi:microphone" - "IsMuted" => state.IsMuted ? "mdi:microphone-off" : "mdi:microphone", - - // If the sensor is "IsVideoOn", return "mdi:camera" if state.IsVideoOn is true, - // otherwise return "mdi:camera-off" - "IsVideoOn" => state.IsVideoOn ? "mdi:camera" : "mdi:camera-off", - - // If the sensor is "IsHandRaised", return "mdi:hand-back-left" if - // state.IsHandRaised is true, otherwise return "mdi:hand-back-left-off" - "IsHandRaised" => state.IsHandRaised ? "mdi:hand-back-left" : "mdi:hand-back-left-off", - - // If the sensor is "IsInMeeting", return "mdi:account-group" if state.IsInMeeting - // is true, otherwise return "mdi:account-off" - "IsInMeeting" => state.IsInMeeting ? "mdi:account-group" : "mdi:account-off", - - // If the sensor is "IsRecordingOn", return "mdi:record-rec" if state.IsRecordingOn - // is true, otherwise return "mdi:record" - "IsRecordingOn" => state.IsRecordingOn ? "mdi:record-rec" : "mdi:record", - - // If the sensor is "IsBackgroundBlurred", return "mdi:blur" if - // state.IsBackgroundBlurred is true, otherwise return "mdi:blur-off" - "IsBackgroundBlurred" => state.IsBackgroundBlurred ? "mdi:blur" : "mdi:blur-off", - - // If the sensor is "IsSharing", return "mdi:monitor-share" if state.IsSharing is - // true, otherwise return "mdi:monitor-off" - "IsSharing" => state.IsSharing ? "mdi:monitor-share" : "mdi:monitor-off", - - // If the sensor is "HasUnreadMessages", return "mdi:message-alert" if - // state.HasUnreadMessages is true, otherwise return "mdi:message-outline" - "HasUnreadMessages" => state.HasUnreadMessages ? "mdi:message-alert" : "mdi:message-outline", - - // If the sensor does not match any of the above cases, return "mdi:eye" - _ => "mdi:eye" - }; - } - - private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) - { - switch (sensor) - { - case "IsMuted": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - - case "IsVideoOn": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - - case "IsBackgroundBlurred": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - - case "IsHandRaised": - // Cast to bool and then check the value - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; - - case "IsInMeeting": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - - case "HasUnreadMessages": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - - case "IsRecordingOn": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - - case "IsSharing": - // Similar casting for these properties - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - - case "teamsRunning": - return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; - - default: - return "unknown"; - } - } - - private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) - { - if (_mqttClientWrapper != null && _mqttClientWrapper.IsConnected) - { - // Example: Publish a keep-alive message - string keepAliveTopic = "TEAMS2HA/keepalive"; - string keepAliveMessage = "alive"; - _ = _mqttClientWrapper.PublishAsync(keepAliveTopic, keepAliveMessage); - Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); - } - } - - private void UpdateMqttClientWrapper() - { - _mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword, - _settings.UseTLS, - _settings.IgnoreCertificateErrors, - _settings.UseWebsockets - ); - // Subscribe to the ConnectionStatusChanged event - _mqttClientWrapper.ConnectionStatusChanged += UpdateConnectionStatus; - } - - #endregion Private Methods - - // Additional extracted methods... - } -} \ No newline at end of file diff --git a/API/MqttService.cs b/API/MqttService.cs new file mode 100644 index 0000000..b69603f --- /dev/null +++ b/API/MqttService.cs @@ -0,0 +1,692 @@ +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Protocol; +using Serilog; +using System; +using System.Security.Cryptography.X509Certificates; +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; +using System.Timers; +using Newtonsoft.Json; +using static System.Windows.Forms.VisualStyles.VisualStyleElement.ListView; +using System.Text; + +namespace TEAMS2HA.API +{ + public class MqttService + { + #region Private Fields + + private const int MaxConnectionRetries = 2; + private const int RetryDelayMilliseconds = 1000; + private readonly string _deviceId; + private AppSettings _settings; + private bool _isAttemptingConnection = false; + private MqttClient _mqttClient; + private MqttClientOptions _mqttOptions; + public delegate Task CommandToTeamsHandler(string jsonMessage); + public event CommandToTeamsHandler CommandToTeams; + private Dictionary _previousSensorStates; + public event Action StatusUpdated; + private List _sensorNames; + private System.Timers.Timer mqttPublishTimer; + + #endregion Private Fields + + #region Public Constructors + + // Constructor + public MqttService(AppSettings settings, string deviceId, List sensorNames) + { + _settings = settings; + _deviceId = deviceId; + _sensorNames = sensorNames; + _previousSensorStates = new Dictionary(); + + InitializeClient(); + InitializeMqttPublishTimer(); + } + + #endregion Public Constructors + + #region Public Events + + public event Action ConnectionAttempting; + + public event Action ConnectionStatusChanged; + + public event Func MessageReceived; + + #endregion Public Events + + #region Public Properties + + public bool IsAttemptingConnection + { + get { return _isAttemptingConnection; } + private set { _isAttemptingConnection = value; } + } + + public bool IsConnected => _mqttClient.IsConnected; + + #endregion Public Properties + + #region Public Methods + + public static List GetEntityNames(string deviceId) + { + var entityNames = new List + { + $"switch.{deviceId}_ismuted", + $"switch.{deviceId}_isvideoon", + $"switch.{deviceId}_ishandraised", + $"binary_sensor.{deviceId}_isrecordingon", + $"binary_sensor.{deviceId}_isinmeeting", + $"binary_sensor.{deviceId}_issharing", + $"binary_sensor.{deviceId}_hasunreadmessages", + $"switch.{deviceId}_isbackgroundblurred", + $"binary_sensor.{deviceId}_teamsRunning" + }; + + return entityNames; + } + + public async Task ConnectAsync() + { + if (_mqttClient.IsConnected || _isAttemptingConnection) + { + Log.Information("MQTT client is already connected or connection attempt is in progress."); + return; + } + + _isAttemptingConnection = true; + ConnectionAttempting?.Invoke("MQTT Status: Connecting..."); + int retryCount = 0; + + while (retryCount < MaxConnectionRetries && !_mqttClient.IsConnected) + { + try + { + Log.Information($"Attempting to connect to MQTT (Attempt {retryCount + 1}/{MaxConnectionRetries})"); + await _mqttClient.ConnectAsync(_mqttOptions); // Corrected line + Log.Information("Connected to MQTT broker."); + if (_mqttClient.IsConnected) + { + ConnectionStatusChanged?.Invoke("MQTT Status: Connected"); + break; // Exit the loop if connected successfully + } + } + 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); // Wait before retrying + } + } + + _isAttemptingConnection = false; + if (!_mqttClient.IsConnected) + { + ConnectionStatusChanged?.Invoke("MQTT Status: Disconnected (Failed to connect)"); + Log.Error("Failed to connect to MQTT broker after several attempts."); + } + } + + + public async Task DisconnectAsync() + { + if (!_mqttClient.IsConnected) + { + Log.Debug("MQTTClient is not connected"); + ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); + return; + } + + try + { + await _mqttClient.DisconnectAsync(); + Log.Information("MQTT Disconnected"); + ConnectionStatusChanged?.Invoke("MQTTClient is not connected"); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to disconnect from MQTT broker: {ex.Message}"); + } + } + + public void Dispose() + { + if (_mqttClient != null) + { + _ = _mqttClient.DisconnectAsync(); // Disconnect asynchronously + _mqttClient.Dispose(); + Log.Information("MQTT Client Disposed"); + } + } + + public void InitializeMqttPublishTimer() + { + mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds + mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; + mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses + mqttPublishTimer.Enabled = true; // Enable the timer + Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); + } + + public async Task PublishAsync(MqttApplicationMessage message) + { + try + { + await _mqttClient.PublishAsync(message, CancellationToken.None); // Note: Add using System.Threading; if CancellationToken is undefined + Log.Information("Publish successful."); + } + catch (Exception ex) + { + Log.Information($"Error during MQTT publish: {ex.Message}"); + } + } + + + public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings settings) + { + if (_mqttClient == null) + { + Log.Debug("MQTT Client Wrapper is not initialized."); + return; + } + // Define common device information for all entities. + var deviceInfo = new + { + ids = new[] { "teams2ha_" + _deviceId }, // Unique device identifier + mf = "Jimmy White", // Manufacturer name + mdl = "Teams2HA Device", // Model + name = _deviceId, // Device name + sw = "v1.0" // Software version + }; + if (meetingUpdate == null) + { + meetingUpdate = new MeetingUpdate + { + MeetingState = new MeetingState + { + IsMuted = false, + IsVideoOn = false, + IsHandRaised = false, + IsInMeeting = false, + IsRecordingOn = false, + IsBackgroundBlurred = false, + IsSharing = false, + HasUnreadMessages = false, + teamsRunning = false + } + }; + } + foreach (var binary_sensor in _sensorNames) + { + string sensorKey = $"{_deviceId}_{binary_sensor}"; + string sensorName = $"{binary_sensor}".ToLower().Replace(" ", "_"); + string deviceClass = DetermineDeviceClass(binary_sensor); + string icon = DetermineIcon(binary_sensor, meetingUpdate.MeetingState); + string stateValue = GetStateValue(binary_sensor, meetingUpdate); + string uniqueId = $"{_deviceId}_{binary_sensor}"; + string configTopic; + if (!_previousSensorStates.TryGetValue(sensorKey, out var previousState) || previousState != stateValue) + { + _previousSensorStates[sensorKey] = stateValue; // Update the stored state + + if (deviceClass == "switch") + { + configTopic = $"homeassistant/switch/{sensorName}/config"; + var switchConfig = new + { + name = sensorName, + unique_id = uniqueId, + device = deviceInfo, + icon = icon, + command_topic = $"homeassistant/switch/{sensorName}/set", + state_topic = $"homeassistant/switch/{sensorName}/state", + payload_on = "ON", + payload_off = "OFF" + }; + var switchConfigMessage = new MqttApplicationMessageBuilder() + .WithTopic(configTopic) + .WithPayload(JsonConvert.SerializeObject(switchConfig)) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(true) + .Build(); + + await PublishAsync(switchConfigMessage); + + var stateMessage = new MqttApplicationMessageBuilder() + .WithTopic(switchConfig.state_topic) + .WithPayload(stateValue) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(true) + .Build(); + + await PublishAsync(stateMessage); + + } + else if (deviceClass == "binary_sensor") + { + configTopic = $"homeassistant/binary_sensor/{sensorName}/config"; + var binarySensorConfig = new + { + name = sensorName, + unique_id = uniqueId, + device = deviceInfo, + icon = icon, + state_topic = $"homeassistant/binary_sensor/{sensorName}/state", + payload_on = "true", // Assuming "True" states map to "ON" + payload_off = "false" // Assuming "False" states map to "OFF" + }; + var binarySensorConfigMessage = new MqttApplicationMessageBuilder() + .WithTopic(configTopic) + .WithPayload(JsonConvert.SerializeObject(binarySensorConfig)) + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(true) + .Build(); + + await PublishAsync(binarySensorConfigMessage); + + var binarySensorStateMessage = new MqttApplicationMessageBuilder() + .WithTopic(binarySensorConfig.state_topic) + .WithPayload(stateValue.ToLowerInvariant()) // Ensure the state value is in the correct format + .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(true) + .Build(); + + await PublishAsync(binarySensorStateMessage); + + } + } + } + } + + public async Task ReconnectAsync() + { + // Consolidated reconnection logic + } + + public async Task SetupMqttSensors() + { + // Create a dummy MeetingUpdate with default values + var dummyMeetingUpdate = new MeetingUpdate + { + MeetingState = new MeetingState + { + IsMuted = false, + IsVideoOn = false, + IsHandRaised = false, + IsInMeeting = false, + IsRecordingOn = false, + IsBackgroundBlurred = false, + IsSharing = false, + HasUnreadMessages = false, + teamsRunning = false + } + }; + + // Call PublishConfigurations with the dummy MeetingUpdate + await PublishConfigurations(dummyMeetingUpdate, _settings); + } + + public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) + { + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() + .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) + .Build(); + try + { + await _mqttClient.SubscribeAsync(subscribeOptions); + } + catch (Exception ex) + { + Log.Information($"Error during MQTT subscribe: {ex.Message}"); + // Depending on the severity, you might want to rethrow the exception or handle it here. + } + Log.Information("Subscribing." + subscribeOptions); + } + + public void UpdateConnectionStatus(string status) + { + OnConnectionStatusChanged(status); + } + + #endregion Public Methods + + #region Protected Methods + + protected virtual void OnConnectionStatusChanged(string status) + { + ConnectionStatusChanged?.Invoke(status); + } + + #endregion Protected Methods + + #region Private Methods + + private string DetermineDeviceClass(string sensor) + { + switch (sensor) + { + case "IsMuted": + case "IsVideoOn": + case "IsHandRaised": + case "IsBackgroundBlurred": + return "switch"; // These are ON/OFF switches + case "IsInMeeting": + case "HasUnreadMessages": + case "IsRecordingOn": + case "IsSharing": + case "teamsRunning": + return "binary_sensor"; // These are true/false sensors + default: + return null; // Or a default device class if appropriate + } + } + + private string DetermineIcon(string sensor, MeetingState state) + { + return sensor switch + { + // If the sensor is "IsMuted", return "mdi:microphone-off" if state.IsMuted is true, + // otherwise return "mdi:microphone" + "IsMuted" => state.IsMuted ? "mdi:microphone-off" : "mdi:microphone", + + // If the sensor is "IsVideoOn", return "mdi:camera" if state.IsVideoOn is true, + // otherwise return "mdi:camera-off" + "IsVideoOn" => state.IsVideoOn ? "mdi:camera" : "mdi:camera-off", + + // If the sensor is "IsHandRaised", return "mdi:hand-back-left" if + // state.IsHandRaised is true, otherwise return "mdi:hand-back-left-off" + "IsHandRaised" => state.IsHandRaised ? "mdi:hand-back-left" : "mdi:hand-back-left-off", + + // If the sensor is "IsInMeeting", return "mdi:account-group" if state.IsInMeeting + // is true, otherwise return "mdi:account-off" + "IsInMeeting" => state.IsInMeeting ? "mdi:account-group" : "mdi:account-off", + + // If the sensor is "IsRecordingOn", return "mdi:record-rec" if state.IsRecordingOn + // is true, otherwise return "mdi:record" + "IsRecordingOn" => state.IsRecordingOn ? "mdi:record-rec" : "mdi:record", + + // If the sensor is "IsBackgroundBlurred", return "mdi:blur" if + // state.IsBackgroundBlurred is true, otherwise return "mdi:blur-off" + "IsBackgroundBlurred" => state.IsBackgroundBlurred ? "mdi:blur" : "mdi:blur-off", + + // If the sensor is "IsSharing", return "mdi:monitor-share" if state.IsSharing is + // true, otherwise return "mdi:monitor-off" + "IsSharing" => state.IsSharing ? "mdi:monitor-share" : "mdi:monitor-off", + + // If the sensor is "HasUnreadMessages", return "mdi:message-alert" if + // state.HasUnreadMessages is true, otherwise return "mdi:message-outline" + "HasUnreadMessages" => state.HasUnreadMessages ? "mdi:message-alert" : "mdi:message-outline", + + // If the sensor does not match any of the above cases, return "mdi:eye" + _ => "mdi:eye" + }; + } + + private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) + { + switch (sensor) + { + case "IsMuted": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsVideoOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsBackgroundBlurred": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsHandRaised": + // Cast to bool and then check the value + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "ON" : "OFF"; + + case "IsInMeeting": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "HasUnreadMessages": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "IsRecordingOn": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "IsSharing": + // Similar casting for these properties + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + case "teamsRunning": + return (bool)meetingUpdate.MeetingState.GetType().GetProperty(sensor).GetValue(meetingUpdate.MeetingState, null) ? "True" : "False"; + + default: + return "unknown"; + } + } + + private async void HandleSwitchCommand(string topic, string command) + { + // Determine which switch is being controlled based on the topic + string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" + int underscoreIndex = switchName.IndexOf('_'); + if (underscoreIndex != -1 && underscoreIndex < switchName.Length - 1) + { + switchName = switchName.Substring(underscoreIndex + 1); + } + string jsonMessage = ""; + switch (switchName) + { + case "ismuted": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-mute\",\"action\":\"toggle-mute\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isvideoon": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"toggle-video\",\"action\":\"toggle-video\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "isbackgroundblurred": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"background-blur\",\"action\":\"toggle-background-blur\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + case "ishandraised": + jsonMessage = $"{{\"apiVersion\":\"1.0.0\",\"service\":\"raise-hand\",\"action\":\"toggle-hand\",\"manufacturer\":\"Jimmy White\",\"device\":\"THFHA\",\"timestamp\":{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()},\"requestId\":1}}"; + break; + + // Add other cases as needed + } + + if (!string.IsNullOrEmpty(jsonMessage)) + { + // Raise the event + CommandToTeams?.Invoke(jsonMessage); + } + } + public async Task UpdateClientOptionsAndReconnect() + { + InitializeClientOptions(); // Method to reinitialize client options with updated settings + await DisconnectAsync(); + await ConnectAsync(); + } + private void InitializeClientOptions() + { + try + { + var factory = new MqttFactory(); + _mqttClient = (MqttClient?)factory.CreateMqttClient(); + + if (!int.TryParse(_settings.MqttPort, out int mqttportInt)) + { + mqttportInt = 1883; // Default MQTT port + Log.Warning($"Invalid MQTT port provided, defaulting to {mqttportInt}"); + } + + var mqttClientOptionsBuilder = new MqttClientOptionsBuilder() + .WithClientId("Teams2HA") + .WithCredentials(_settings.MqttUsername, _settings.MqttPassword) + .WithCleanSession() + .WithTimeout(TimeSpan.FromSeconds(5)); + + string protocol = _settings.UseWebsockets ? "ws" : "tcp"; + string connectionType = _settings.UseTLS ? "with TLS" : "without TLS"; + + if (_settings.UseWebsockets) + { + string websocketUri = _settings.UseTLS ? $"wss://{_settings.MqttAddress}:{mqttportInt}" : $"ws://{_settings.MqttAddress}:{mqttportInt}"; + mqttClientOptionsBuilder.WithWebSocketServer(websocketUri); + Log.Information($"Configuring MQTT client for WebSocket {connectionType} connection to {websocketUri}"); + } + else + { + mqttClientOptionsBuilder.WithTcpServer(_settings.MqttAddress, mqttportInt); + Log.Information($"Configuring MQTT client for TCP {connectionType} connection to {_settings.MqttAddress}:{mqttportInt}"); + } + + if (_settings.UseTLS) + { + mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters + { + UseTls = true, + AllowUntrustedCertificates = _settings.IgnoreCertificateErrors, + IgnoreCertificateChainErrors = _settings.IgnoreCertificateErrors, + IgnoreCertificateRevocationErrors = _settings.IgnoreCertificateErrors, + CertificateValidationHandler = context => + { + // Log the certificate subject + Log.Debug("Certificate Subject: {0}", context.Certificate.Subject); + + // This assumes you are trying to inspect the certificate directly; + // MQTTnet may not provide a direct IsValid flag or ChainErrors like + // .NET's X509Chain. Instead, you handle validation and log details manually: + + bool isValid = true; // You should define the logic to set this based on your validation requirements + + // Check for specific conditions, if necessary, such as expiry, issuer, etc. + // For example, if you want to ensure the certificate is issued by a specific entity: + //if (context.Certificate.Issuer != "CN=R3, O=Let's Encrypt, C=US") + //{ + // Log.Debug("Unexpected certificate issuer: {0}", context.Certificate.Issuer); + // isValid = false; // Set to false if the issuer is not the expected one + //} + + // Log any errors from the SSL policy errors if they exist + if (context.SslPolicyErrors != System.Net.Security.SslPolicyErrors.None) + { + Log.Debug("SSL policy errors: {0}", context.SslPolicyErrors.ToString()); + isValid = false; // Consider invalid if there are any SSL policy errors + } + + // You can decide to ignore certain errors by setting isValid to true + // regardless of the checks, but be careful as this might introduce + // security vulnerabilities. + if (_settings.IgnoreCertificateErrors) + { + isValid = true; // Ignore certificate errors if your settings dictate + } + + return isValid; // Return the result of your checks + } + }); + } + + _mqttOptions = mqttClientOptionsBuilder.Build(); + if (_mqttClient != null) + { + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to initialize MqttClientWrapper"); + throw; // Rethrowing the exception to handle it outside or log it as fatal depending on your error handling strategy. + } + } + private void InitializeClient() + { + if (_mqttClient == null) + { + var factory = new MqttFactory(); + _mqttClient = (MqttClient?)factory.CreateMqttClient(); // This creates an IMqttClient, not a MqttClient. + + InitializeClientOptions(); // Ensure options are initialized with current settings + + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + } + } + + public async Task UpdateSettingsAsync(AppSettings newSettings) + { + _settings = newSettings; + InitializeClientOptions(); // Reinitialize MQTT client options + + if (IsConnected) + { + await DisconnectAsync(); + await ConnectAsync(); + } + } + + + + private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) + { + Log.Information($"Received message on topic {e.ApplicationMessage.Topic}: {e.ApplicationMessage.ConvertPayloadToString()}"); + string topic = e.ApplicationMessage.Topic; + string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); + + // Assuming the format is homeassistant/switch/{deviceId}/{switchName}/set + // Validate the topic format and extract the switchName + var topicParts = topic.Split(','); + topicParts = topic.Split('/'); + if (topicParts.Length == 4 && topicParts[0].Equals("homeassistant") && topicParts[1].Equals("switch") && topicParts[3].EndsWith("set")) + { + // Extract the action and switch name from the topic + string switchName = topicParts[2]; + string command = payload; // command should be ON or OFF based on the payload + + // Now call the handle method + HandleSwitchCommand(topic, command); + } + + return Task.CompletedTask; + } + + + + private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) + { + if (_mqttClient != null && _mqttClient.IsConnected) + { + // Example: Publish a keep-alive message + string keepAliveTopic = "TEAMS2HA/keepalive"; + string keepAliveMessage = "alive"; + + // Create the MQTT message + var message = new MqttApplicationMessageBuilder() + .WithTopic(keepAliveTopic) + .WithPayload(keepAliveMessage) + .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce) // Or another QoS level if required + .Build(); + + // Publish the message asynchronously + _ = _mqttClient.PublishAsync(message); + + Log.Debug("OnMqttPublishTimerElapsed: MQTT Keep Alive Message Published"); + } + } + + + + + #endregion Private Methods + + // Additional methods for sensor management, message handling, etc. + } +} \ No newline at end of file diff --git a/AboutWindow.xaml.cs b/AboutWindow.xaml.cs index a0ef4d0..6b5fedd 100644 --- a/AboutWindow.xaml.cs +++ b/AboutWindow.xaml.cs @@ -12,7 +12,7 @@ public partial class AboutWindow : Window #region Private Fields private TaskbarIcon _notifyIcon; - + private MqttService _mqttService; #endregion Private Fields #region Public Constructors @@ -25,7 +25,8 @@ public AboutWindow(string deviceId, TaskbarIcon notifyIcon) _notifyIcon = notifyIcon; SetVersionInfo(); - var entityNames = MqttClientWrapper.GetEntityNames(deviceId); + var entityNames = MqttService.GetEntityNames(deviceId); + EntitiesListBox.ItemsSource = entityNames; } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 92d4921..86296d2 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -216,7 +216,8 @@ public partial class MainWindow : Window private MeetingUpdate _latestMeetingUpdate; private MenuItem _logMenuItem; - private MqttManager _mqttManager; + //private MqttManager _mqttManager; + private MqttService _mqttService; private MenuItem _mqttStatusMenuItem; //private string Mqtttopic; @@ -230,7 +231,7 @@ public partial class MainWindow : Window private Action _updateTokenAction; private string deviceid; private bool isDarkTheme = false; - private MqttClientWrapper mqttClientWrapper; + private List sensorNames = new List { @@ -287,23 +288,14 @@ public MainWindow() string iconPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Square150x150Logo.scale-200.ico"); MyNotifyIcon.Icon = new System.Drawing.Icon(iconPath); CreateNotifyIconContextMenu(); - // Create a new instance of the MqttClientWrapper class - mqttClientWrapper = new MqttClientWrapper( - "TEAMS2HA", - _settings.MqttAddress, - _settings.MqttPort, - _settings.MqttUsername, - _settings.MqttPassword, - _settings.UseTLS, - _settings.IgnoreCertificateErrors, - _settings.UseWebsockets - ); - _mqttManager = new MqttManager(mqttClientWrapper, settings, sensorNames, deviceid); - _mqttManager.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; - _mqttManager.StatusUpdated += UpdateMqttStatus; - _mqttManager.CommandToTeams += HandleCommandToTeams; - mqttClientWrapper.ConnectionAttempting += MqttManager_ConnectionAttempting; - + // Create a new instance of the MQTT Service class + + _mqttService = new MqttService(settings, deviceid, sensorNames); + _mqttService.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; + _mqttService.StatusUpdated += UpdateMqttStatus; + _mqttService.CommandToTeams += HandleCommandToTeams; + _mqttService.ConnectionAttempting += MqttManager_ConnectionAttempting; + // Set the action to be performed when a new token is updated _updateTokenAction = newToken => { @@ -323,7 +315,7 @@ public MainWindow() // Initialize the MQTT publish timer - _mqttManager.InitializeMqttPublishTimer(); + _mqttService.InitializeMqttPublishTimer(); } #endregion Public Constructors @@ -332,7 +324,9 @@ public MainWindow() public async Task InitializeConnections() { - await _mqttManager.InitializeConnection(); + await _mqttService.ConnectAsync(); + + await _mqttService.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); // Other initialization code... await initializeteamsconnection(); @@ -348,11 +342,11 @@ protected override async void OnClosing(CancelEventArgs e) // Unsubscribe from events and clean up - if (_mqttManager != null) + if (_mqttService != null) { - _mqttManager.ConnectionStatusChanged -= MqttManager_ConnectionStatusChanged; - _mqttManager.StatusUpdated -= UpdateMqttStatus; - _mqttManager.CommandToTeams -= HandleCommandToTeams; + _mqttService.ConnectionStatusChanged -= MqttManager_ConnectionStatusChanged; + _mqttService.StatusUpdated -= UpdateMqttStatus; + _mqttService.CommandToTeams -= HandleCommandToTeams; } if (_teamsClient != null) { @@ -360,11 +354,11 @@ protected override async void OnClosing(CancelEventArgs e) Log.Debug("Teams Client Disconnected"); } // we want all the sensors to be off if we are exiting, lets initialise them, to do this - await _mqttManager.SetupMqttSensors(); - if (mqttClientWrapper != null) + await _mqttService.SetupMqttSensors(); + if (_mqttService != null) { - await mqttClientWrapper.DisconnectAsync(); // Properly disconnect before disposing - mqttClientWrapper.Dispose(); + await _mqttService.DisconnectAsync(); // Properly disconnect before disposing + _mqttService.Dispose(); Log.Debug("MQTT Client Disposed"); } MyNotifyIcon.Dispose(); @@ -467,11 +461,11 @@ private bool CheckIfSensorPrefixChanged(AppSettings newSettings) private async void CheckMqttConnection() { - if (mqttClientWrapper != null && !mqttClientWrapper.IsConnected && !mqttClientWrapper.IsAttemptingConnection) + if (_mqttService != null && !_mqttService.IsConnected && !_mqttService.IsAttemptingConnection) { Log.Debug("CheckMqttConnection: MQTT Client Not Connected. Attempting reconnection."); - await mqttClientWrapper.ConnectAsync(); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + await _mqttService.ConnectAsync(); + await _mqttService.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); //UpdateConnectionStatus(); UpdateStatusMenuItems(); } @@ -706,11 +700,11 @@ private async void ReestablishConnections() { try { - if (!mqttClientWrapper.IsConnected) + if (!_mqttService.IsConnected) { - await mqttClientWrapper.ConnectAsync(); - await mqttClientWrapper.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - await _mqttManager.SetupMqttSensors(); + await _mqttService.ConnectAsync(); + await _mqttService.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + await _mqttService.SetupMqttSensors(); Dispatcher.Invoke(() => UpdateStatusMenuItems()); } if (!_teamsClient.IsConnected) @@ -767,14 +761,12 @@ private async Task SaveSettingsAsync() // Save the updated settings to file settings.SaveSettingsToFile(); - - await _mqttManager.ReconnectToMqttServerAsync(); - await _mqttManager.SetupMqttSensors(); - await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); - } - - private async Task SetStartupAsync(bool startWithWindows) - { + await _mqttService.DisconnectAsync(); + await _mqttService.UpdateSettingsAsync(AppSettings.Instance); + await _mqttService.ConnectAsync(); + await _mqttService.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); + await _mqttService.SetupMqttSensors(); + await _mqttService.PublishConfigurations(_latestMeetingUpdate, _settings); } private void SetWindowTitle() @@ -798,13 +790,13 @@ private void ShowHideMenuItem_Click(object sender, RoutedEventArgs e) private async void TeamsClient_TeamsUpdateReceived(object sender, WebSocketClient.TeamsUpdateEventArgs e) { - if (mqttClientWrapper != null && mqttClientWrapper.IsConnected) + if (_mqttService != null && _mqttService.IsConnected) { // Store the latest update _latestMeetingUpdate = e.MeetingUpdate; Log.Debug("TeamsClient_TeamsUpdateReceived: Teams Update Received {update}", _latestMeetingUpdate); // Update sensor configurations - await _mqttManager.PublishConfigurations(_latestMeetingUpdate, _settings); + await _mqttService.PublishConfigurations(_latestMeetingUpdate, _settings); } } @@ -822,7 +814,7 @@ private void TeamsConnectionStatusChanged(bool isConnected) else { State.Instance.teamsRunning = false; - _ = _mqttManager.PublishConfigurations(null, _settings); + _ = _mqttService.PublishConfigurations(null, _settings); } Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); @@ -859,21 +851,6 @@ private void ToggleThemeButton_Click(object sender, RoutedEventArgs e) _ = SaveSettingsAsync(); } - //private void UpdateConnectionStatus() - //{ - // Dispatcher.Invoke(() => - // { - // MQTTConnectionStatus.Text = mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; - // Dispatcher.Invoke(() => UpdateStatusMenuItems()); - // }); - //} - - //private void UpdateMqttConnectionStatus(string status) - //{ - // Dispatcher.Invoke(() => MQTTConnectionStatus.Text = status); - // Dispatcher.Invoke(() => UpdateStatusMenuItems()); - //} - private void UpdateMqttStatus(string status) { Dispatcher.Invoke(() => @@ -889,7 +866,7 @@ private void UpdateStatusMenuItems() Dispatcher.Invoke(() => { // Update MQTT connection status text - MQTTConnectionStatus.Text = mqttClientWrapper != null && mqttClientWrapper.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; + MQTTConnectionStatus.Text = _mqttService != null && _mqttService.IsConnected ? "MQTT Status: Connected" : "MQTT Status: Disconnected"; // Update menu items _mqttStatusMenuItem.Header = MQTTConnectionStatus.Text; // Reuse the text set above _teamsStatusMenuItem.Header = _teamsClient != null && _teamsClient.IsConnected ? "Teams Status: Connected" : "Teams Status: Disconnected"; diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index f54b63b..4c51e45 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.405 - 1.1.0.405 + 1.1.0.425 + 1.1.0.425 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 357644ab45b9151739195b2fd365645f57966d7a Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Mon, 11 Mar 2024 16:37:58 +0000 Subject: [PATCH 18/31] updates to MQTT --- API/MqttService.cs | 60 +++++++++++++++++----------------------------- MainWindow.xaml.cs | 1 + TEAMS2HA.csproj | 5 ++-- 3 files changed, 26 insertions(+), 40 deletions(-) diff --git a/API/MqttService.cs b/API/MqttService.cs index b69603f..ce514e8 100644 --- a/API/MqttService.cs +++ b/API/MqttService.cs @@ -551,49 +551,33 @@ private void InitializeClientOptions() if (_settings.UseTLS) { - mqttClientOptionsBuilder.WithTls(new MqttClientOptionsBuilderTlsParameters + // Create TLS parameters + var tlsParameters = new MqttClientOptionsBuilderTlsParameters { - UseTls = true, AllowUntrustedCertificates = _settings.IgnoreCertificateErrors, IgnoreCertificateChainErrors = _settings.IgnoreCertificateErrors, IgnoreCertificateRevocationErrors = _settings.IgnoreCertificateErrors, - CertificateValidationHandler = context => + UseTls = true + }; + + // If you need to validate the server certificate, you can set the CertificateValidationHandler. + // Note: Be cautious with bypassing certificate checks in production code. + if (!_settings.IgnoreCertificateErrors) + { + tlsParameters.CertificateValidationHandler = context => { - // Log the certificate subject - Log.Debug("Certificate Subject: {0}", context.Certificate.Subject); - - // This assumes you are trying to inspect the certificate directly; - // MQTTnet may not provide a direct IsValid flag or ChainErrors like - // .NET's X509Chain. Instead, you handle validation and log details manually: - - bool isValid = true; // You should define the logic to set this based on your validation requirements - - // Check for specific conditions, if necessary, such as expiry, issuer, etc. - // For example, if you want to ensure the certificate is issued by a specific entity: - //if (context.Certificate.Issuer != "CN=R3, O=Let's Encrypt, C=US") - //{ - // Log.Debug("Unexpected certificate issuer: {0}", context.Certificate.Issuer); - // isValid = false; // Set to false if the issuer is not the expected one - //} - - // Log any errors from the SSL policy errors if they exist - if (context.SslPolicyErrors != System.Net.Security.SslPolicyErrors.None) - { - Log.Debug("SSL policy errors: {0}", context.SslPolicyErrors.ToString()); - isValid = false; // Consider invalid if there are any SSL policy errors - } - - // You can decide to ignore certain errors by setting isValid to true - // regardless of the checks, but be careful as this might introduce - // security vulnerabilities. - if (_settings.IgnoreCertificateErrors) - { - isValid = true; // Ignore certificate errors if your settings dictate - } - - return isValid; // Return the result of your checks - } - }); + // Log the SSL policy errors + Log.Debug($"SSL policy errors: {context.SslPolicyErrors}"); + + // Return true if there are no SSL policy errors, or if ignoring certificate errors is allowed + return context.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None; + }; + } + + + // Apply the TLS parameters to the options builder + mqttClientOptionsBuilder.WithTls(tlsParameters); + } _mqttOptions = mqttClientOptionsBuilder.Build(); diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 86296d2..34ac546 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -18,6 +18,7 @@ using System.Windows; using TEAMS2HA.API; using TEAMS2HA.Properties; +using MaterialDesignThemes.Wpf; namespace TEAMS2HA { diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 4c51e45..e62e89f 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.425 - 1.1.0.425 + 1.1.0.453 + 1.1.0.453 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png @@ -36,6 +36,7 @@ + From 1561e24df18e301a4e159249eb79ba7d9bb0adb4 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Tue, 12 Mar 2024 12:42:44 +0000 Subject: [PATCH 19/31] Prevent multiple mqtt subscriptions during reconnect --- API/MqttService.cs | 114 +++++++++++++++++++++++---------------------- TEAMS2HA.csproj | 5 +- 2 files changed, 61 insertions(+), 58 deletions(-) diff --git a/API/MqttService.cs b/API/MqttService.cs index ce514e8..d0e1afa 100644 --- a/API/MqttService.cs +++ b/API/MqttService.cs @@ -26,23 +26,27 @@ public class MqttService private const int MaxConnectionRetries = 2; private const int RetryDelayMilliseconds = 1000; private readonly string _deviceId; - private AppSettings _settings; private bool _isAttemptingConnection = false; private MqttClient _mqttClient; private MqttClientOptions _mqttOptions; - public delegate Task CommandToTeamsHandler(string jsonMessage); - public event CommandToTeamsHandler CommandToTeams; private Dictionary _previousSensorStates; - public event Action StatusUpdated; private List _sensorNames; + private AppSettings _settings; private System.Timers.Timer mqttPublishTimer; + private HashSet _subscribedTopics = new HashSet(); + + public delegate Task CommandToTeamsHandler(string jsonMessage); + + public event CommandToTeamsHandler CommandToTeams; + + public event Action StatusUpdated; #endregion Private Fields #region Public Constructors // Constructor - public MqttService(AppSettings settings, string deviceId, List sensorNames) + public MqttService(AppSettings settings, string deviceId, List sensorNames) { _settings = settings; _deviceId = deviceId; @@ -78,7 +82,24 @@ public bool IsAttemptingConnection #endregion Public Properties #region Public Methods + public async Task UpdateClientOptionsAndReconnect() + { + InitializeClientOptions(); // Method to reinitialize client options with updated settings + await DisconnectAsync(); + await ConnectAsync(); + } + public async Task UpdateSettingsAsync(AppSettings newSettings) + { + _settings = newSettings; + InitializeClientOptions(); // Reinitialize MQTT client options + + if (IsConnected) + { + await DisconnectAsync(); + await ConnectAsync(); + } + } public static List GetEntityNames(string deviceId) { var entityNames = new List @@ -139,7 +160,6 @@ public async Task ConnectAsync() } } - public async Task DisconnectAsync() { if (!_mqttClient.IsConnected) @@ -185,7 +205,7 @@ public async Task PublishAsync(MqttApplicationMessage message) try { await _mqttClient.PublishAsync(message, CancellationToken.None); // Note: Add using System.Threading; if CancellationToken is undefined - Log.Information("Publish successful."); + Log.Information("Publish successful." + message.Topic); } catch (Exception ex) { @@ -193,7 +213,6 @@ public async Task PublishAsync(MqttApplicationMessage message) } } - public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings settings) { if (_mqttClient == null) @@ -243,7 +262,7 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings if (deviceClass == "switch") { - configTopic = $"homeassistant/switch/{sensorName}/config"; + configTopic = $"homeassistant/switch/{sensorName}/config"; var switchConfig = new { name = sensorName, @@ -272,7 +291,6 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings .Build(); await PublishAsync(stateMessage); - } else if (deviceClass == "binary_sensor") { @@ -304,7 +322,6 @@ public async Task PublishConfigurations(MeetingUpdate meetingUpdate, AppSettings .Build(); await PublishAsync(binarySensorStateMessage); - } } } @@ -340,21 +357,30 @@ public async Task SetupMqttSensors() public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) { + // Check if already subscribed + if (_subscribedTopics.Contains(topic)) + { + Log.Information($"Already subscribed to {topic}."); + return; + } + var subscribeOptions = new MqttClientSubscribeOptionsBuilder() - .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) - .Build(); + .WithTopicFilter(f => f.WithTopic(topic).WithQualityOfServiceLevel(qos)) + .Build(); + try { await _mqttClient.SubscribeAsync(subscribeOptions); + _subscribedTopics.Add(topic); // Track the subscription + Log.Information("Subscribed to " + topic); } catch (Exception ex) { Log.Information($"Error during MQTT subscribe: {ex.Message}"); - // Depending on the severity, you might want to rethrow the exception or handle it here. } - Log.Information("Subscribing." + subscribeOptions); } + public void UpdateConnectionStatus(string status) { OnConnectionStatusChanged(status); @@ -373,6 +399,8 @@ protected virtual void OnConnectionStatusChanged(string status) #region Private Methods + + private string DetermineDeviceClass(string sensor) { switch (sensor) @@ -509,12 +537,20 @@ private async void HandleSwitchCommand(string topic, string command) CommandToTeams?.Invoke(jsonMessage); } } - public async Task UpdateClientOptionsAndReconnect() + + private void InitializeClient() { - InitializeClientOptions(); // Method to reinitialize client options with updated settings - await DisconnectAsync(); - await ConnectAsync(); + if (_mqttClient == null) + { + var factory = new MqttFactory(); + _mqttClient = (MqttClient?)factory.CreateMqttClient(); // This creates an IMqttClient, not a MqttClient. + + InitializeClientOptions(); // Ensure options are initialized with current settings + + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + } } + private void InitializeClientOptions() { try @@ -569,15 +605,14 @@ private void InitializeClientOptions() // Log the SSL policy errors Log.Debug($"SSL policy errors: {context.SslPolicyErrors}"); - // Return true if there are no SSL policy errors, or if ignoring certificate errors is allowed + // Return true if there are no SSL policy errors, or if ignoring + // certificate errors is allowed return context.SslPolicyErrors == System.Net.Security.SslPolicyErrors.None; }; } - // Apply the TLS parameters to the options builder mqttClientOptionsBuilder.WithTls(tlsParameters); - } _mqttOptions = mqttClientOptionsBuilder.Build(); @@ -592,32 +627,6 @@ private void InitializeClientOptions() throw; // Rethrowing the exception to handle it outside or log it as fatal depending on your error handling strategy. } } - private void InitializeClient() - { - if (_mqttClient == null) - { - var factory = new MqttFactory(); - _mqttClient = (MqttClient?)factory.CreateMqttClient(); // This creates an IMqttClient, not a MqttClient. - - InitializeClientOptions(); // Ensure options are initialized with current settings - - _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; - } - } - - public async Task UpdateSettingsAsync(AppSettings newSettings) - { - _settings = newSettings; - InitializeClientOptions(); // Reinitialize MQTT client options - - if (IsConnected) - { - await DisconnectAsync(); - await ConnectAsync(); - } - } - - private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) { @@ -625,8 +634,8 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) string topic = e.ApplicationMessage.Topic; string payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload); - // Assuming the format is homeassistant/switch/{deviceId}/{switchName}/set - // Validate the topic format and extract the switchName + // Assuming the format is homeassistant/switch/{deviceId}/{switchName}/set Validate the + // topic format and extract the switchName var topicParts = topic.Split(','); topicParts = topic.Split('/'); if (topicParts.Length == 4 && topicParts[0].Equals("homeassistant") && topicParts[1].Equals("switch") && topicParts[3].EndsWith("set")) @@ -642,8 +651,6 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) return Task.CompletedTask; } - - private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) { if (_mqttClient != null && _mqttClient.IsConnected) @@ -666,9 +673,6 @@ private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) } } - - - #endregion Private Methods // Additional methods for sensor management, message handling, etc. diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index e62e89f..0d3b0d2 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.453 - 1.1.0.453 + 1.1.0.458 + 1.1.0.458 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png @@ -36,7 +36,6 @@ - From 32b4f23321edcb736b35c78efe9ef4d7ffc05f15 Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Tue, 12 Mar 2024 13:38:21 +0000 Subject: [PATCH 20/31] added logic to prevent mutiple subscriptions --- API/MqttService.cs | 58 ++++++++++++++++++++++++++++++++-------------- API/Teams.cs | 16 ++++++------- MainWindow.xaml.cs | 56 +++++++++++++++++++++++++++++++++----------- TEAMS2HA.csproj | 4 ++-- 4 files changed, 92 insertions(+), 42 deletions(-) diff --git a/API/MqttService.cs b/API/MqttService.cs index d0e1afa..25e4fab 100644 --- a/API/MqttService.cs +++ b/API/MqttService.cs @@ -34,9 +34,9 @@ public class MqttService private AppSettings _settings; private System.Timers.Timer mqttPublishTimer; private HashSet _subscribedTopics = new HashSet(); - + private bool mqttPublishTimerset = false; public delegate Task CommandToTeamsHandler(string jsonMessage); - + private bool _mqttClientsubscribed = false; public event CommandToTeamsHandler CommandToTeams; public event Action StatusUpdated; @@ -120,6 +120,7 @@ public static List GetEntityNames(string deviceId) public async Task ConnectAsync() { + // Check if MQTT client is already connected or connection attempt is in progress if (_mqttClient.IsConnected || _isAttemptingConnection) { Log.Information("MQTT client is already connected or connection attempt is in progress."); @@ -130,17 +131,18 @@ public async Task ConnectAsync() ConnectionAttempting?.Invoke("MQTT Status: Connecting..."); int retryCount = 0; + // Retry connecting to MQTT broker up to a maximum number of times while (retryCount < MaxConnectionRetries && !_mqttClient.IsConnected) { try { Log.Information($"Attempting to connect to MQTT (Attempt {retryCount + 1}/{MaxConnectionRetries})"); - await _mqttClient.ConnectAsync(_mqttOptions); // Corrected line + await _mqttClient.ConnectAsync(_mqttOptions); Log.Information("Connected to MQTT broker."); if (_mqttClient.IsConnected) { ConnectionStatusChanged?.Invoke("MQTT Status: Connected"); - break; // Exit the loop if connected successfully + break; // Exit the loop if successfully connected } } catch (Exception ex) @@ -148,11 +150,12 @@ public async Task ConnectAsync() 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); // Wait before retrying + await Task.Delay(RetryDelayMilliseconds); // Delay before retrying } } _isAttemptingConnection = false; + // Notify if failed to connect after all retry attempts if (!_mqttClient.IsConnected) { ConnectionStatusChanged?.Invoke("MQTT Status: Disconnected (Failed to connect)"); @@ -194,10 +197,22 @@ public void Dispose() public void InitializeMqttPublishTimer() { mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds - mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; - mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses - mqttPublishTimer.Enabled = true; // Enable the timer - Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); + if(mqttPublishTimerset == false) + { + mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; + mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses + mqttPublishTimer.Enabled = true; // Enable the timer + mqttPublishTimerset = true; + Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); + } + else + { + Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer already set"); + } + //mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; + //mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses + //mqttPublishTimer.Enabled = true; // Enable the timer + //Log.Debug("InitializeMqttPublishTimer: MQTT Publish Timer Initialized"); } public async Task PublishAsync(MqttApplicationMessage message) @@ -417,7 +432,7 @@ private string DetermineDeviceClass(string sensor) case "teamsRunning": return "binary_sensor"; // These are true/false sensors default: - return null; // Or a default device class if appropriate + return "unknown"; // Or a default device class if appropriate } } @@ -500,7 +515,7 @@ private string GetStateValue(string sensor, MeetingUpdate meetingUpdate) } } - private async void HandleSwitchCommand(string topic, string command) + private void HandleSwitchCommand(string topic, string command) { // Determine which switch is being controlled based on the topic string switchName = topic.Split('/')[2]; // Assuming topic format is "homeassistant/switch/{switchName}/set" @@ -546,8 +561,12 @@ private void InitializeClient() _mqttClient = (MqttClient?)factory.CreateMqttClient(); // This creates an IMqttClient, not a MqttClient. InitializeClientOptions(); // Ensure options are initialized with current settings - - _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + if(_mqttClientsubscribed == false) + { + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + _mqttClientsubscribed = true; + } + } } @@ -597,7 +616,7 @@ private void InitializeClientOptions() }; // If you need to validate the server certificate, you can set the CertificateValidationHandler. - // Note: Be cautious with bypassing certificate checks in production code. + // Note: Be cautious with bypassing certificate checks in production code!! if (!_settings.IgnoreCertificateErrors) { tlsParameters.CertificateValidationHandler = context => @@ -618,7 +637,12 @@ private void InitializeClientOptions() _mqttOptions = mqttClientOptionsBuilder.Build(); if (_mqttClient != null) { - _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + if(_mqttClientsubscribed == false) + { + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + _mqttClientsubscribed = true; + } + } } catch (Exception ex) @@ -636,7 +660,7 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) // Assuming the format is homeassistant/switch/{deviceId}/{switchName}/set Validate the // topic format and extract the switchName - var topicParts = topic.Split(','); + var topicParts = topic.Split(','); //not sure this is required topicParts = topic.Split('/'); if (topicParts.Length == 4 && topicParts[0].Equals("homeassistant") && topicParts[1].Equals("switch") && topicParts[3].EndsWith("set")) { @@ -645,7 +669,7 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) string command = payload; // command should be ON or OFF based on the payload // Now call the handle method - HandleSwitchCommand(topic, command); + HandleSwitchCommand(topic, command); } return Task.CompletedTask; diff --git a/API/Teams.cs b/API/Teams.cs index 5b4c860..6e28a9e 100644 --- a/API/Teams.cs +++ b/API/Teams.cs @@ -102,7 +102,6 @@ public async Task ConnectAsync(Uri uri) if (!string.IsNullOrEmpty(token)) { - // Modify the URI to include the token Log.Debug($"Token: {token}"); var builder = new UriBuilder(uri) { Query = $"token={token}&{uri.Query.TrimStart('?')}" }; uri = builder.Uri; @@ -208,11 +207,10 @@ private void OnMessageReceived(object sender, string message) _updateTokenAction?.Invoke(AppSettings.Instance.PlainTeamsToken); }); } - else if (message.Contains("meetingPermissions")) // Replace with actual keyword/structure + else if (message.Contains("meetingPermissions")) { Log.Debug("Pairing..."); - // Update UI, save settings, reinitialize connection as needed - + // Update the Message property of the State class var settings = new JsonSerializerSettings { @@ -225,7 +223,7 @@ private void OnMessageReceived(object sender, string message) { // The 'canPair' permission is true, initiate pairing Log.Debug("Pairing with Teams"); - PairWithTeamsAsync(); + _= PairWithTeamsAsync(); } // Update the meeting state dictionary if (meetingUpdate.MeetingState != null) @@ -421,7 +419,7 @@ public class MeetingUpdateConverter : JsonConverter { #region Public Methods - public override MeetingUpdate ReadJson(JsonReader reader, Type objectType, MeetingUpdate existingValue, bool hasExistingValue, JsonSerializer serializer) + public override MeetingUpdate ReadJson(JsonReader reader, Type objectType, MeetingUpdate? existingValue, bool hasExistingValue, JsonSerializer serializer) { JObject jsonObject = JObject.Load(reader); MeetingState meetingState = null; @@ -453,7 +451,7 @@ public override MeetingUpdate ReadJson(JsonReader reader, Type objectType, Meeti }; } - public override void WriteJson(JsonWriter writer, MeetingUpdate value, JsonSerializer serializer) + public override void WriteJson(JsonWriter writer, MeetingUpdate? value, JsonSerializer serializer) { throw new NotImplementedException(); } @@ -465,7 +463,7 @@ public class TeamsUpdateEventArgs : EventArgs { #region Public Properties - public MeetingUpdate MeetingUpdate { get; set; } + public MeetingUpdate? MeetingUpdate { get; set; } #endregion Public Properties } @@ -475,7 +473,7 @@ public class TokenUpdate #region Public Properties [JsonProperty("tokenRefresh")] - public string NewToken { get; set; } + public string? NewToken { get; set; } #endregion Public Properties } diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 34ac546..0ae8f04 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -232,8 +232,12 @@ public partial class MainWindow : Window private Action _updateTokenAction; private string deviceid; private bool isDarkTheme = false; - - + private bool isTeamsSubscribed = false; + private bool isTeamsConnected = false; + private bool mqttConnectionStatusChanged = false; + private bool mqttStatusUpdated = false; + private bool mqttCommandToTeams = false; + private bool mqttConnectionAttempting = false; private List sensorNames = new List { "IsMuted", "IsVideoOn", "IsHandRaised", "IsInMeeting", "IsRecordingOn", "IsBackgroundBlurred", "IsSharing", "HasUnreadMessages", "teamsRunning" @@ -290,12 +294,28 @@ public MainWindow() MyNotifyIcon.Icon = new System.Drawing.Icon(iconPath); CreateNotifyIconContextMenu(); // Create a new instance of the MQTT Service class - + if(mqttConnectionStatusChanged == false) + { + _mqttService = new MqttService(settings, deviceid, sensorNames); + mqttConnectionStatusChanged = true; + } _mqttService = new MqttService(settings, deviceid, sensorNames); - _mqttService.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; - _mqttService.StatusUpdated += UpdateMqttStatus; - _mqttService.CommandToTeams += HandleCommandToTeams; - _mqttService.ConnectionAttempting += MqttManager_ConnectionAttempting; + if(mqttStatusUpdated == false) + { + _mqttService.ConnectionStatusChanged += MqttManager_ConnectionStatusChanged; + mqttStatusUpdated = true; + } + if(mqttCommandToTeams == false) + { + _mqttService.CommandToTeams += HandleCommandToTeams; + mqttCommandToTeams = true; + } + if(mqttConnectionAttempting == false) + { + _mqttService.ConnectionAttempting += MqttManager_ConnectionAttempting; + mqttConnectionAttempting = true; + } + // Set the action to be performed when a new token is updated _updateTokenAction = newToken => @@ -314,9 +334,6 @@ public MainWindow() _previousSensorStates[$"{deviceid}_{sensor}"] = ""; } - - // Initialize the MQTT publish timer - _mqttService.InitializeMqttPublishTimer(); } #endregion Public Constructors @@ -539,8 +556,18 @@ private async Task initializeteamsconnection() _settingsFilePath, _updateTokenAction // Pass the action here ); - _teamsClient.ConnectionStatusChanged += TeamsConnectionStatusChanged; - _teamsClient.TeamsUpdateReceived += TeamsClient_TeamsUpdateReceived; + if(isTeamsConnected == false) + { + _teamsClient.ConnectionStatusChanged += TeamsConnectionStatusChanged; + isTeamsConnected = true; + } + + if(isTeamsSubscribed == false) + { + _teamsClient.TeamsUpdateReceived += TeamsClient_TeamsUpdateReceived; + isTeamsSubscribed = true; + } + } // Connect if not already connected @@ -722,7 +749,8 @@ private async void ReestablishConnections() private async void SaveSettings_Click(object sender, RoutedEventArgs e) { - Log.Debug("SaveSettings_Click: Save Settings Clicked" + _settings.ToString); + Log.Debug("SaveSettings_Click: Save Settings Clicked" + _settings.ToString()); + // uncomment below for testing ** insecure as tokens exposed in logs! ** //foreach(var setting in _settings.GetType().GetProperties()) //{ @@ -815,7 +843,7 @@ private void TeamsConnectionStatusChanged(bool isConnected) else { State.Instance.teamsRunning = false; - _ = _mqttService.PublishConfigurations(null, _settings); + _ = _mqttService.PublishConfigurations(null!, _settings); } Log.Debug("TeamsConnectionStatusChanged: Teams Connection Status Changed {status}", TeamsConnectionStatus.Text); diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index 0d3b0d2..b09243c 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.458 - 1.1.0.458 + 1.1.0.462 + 1.1.0.462 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From 9b49c102f7a540fdd059adcf4e23881857aac0be Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Tue, 12 Mar 2024 13:48:28 +0000 Subject: [PATCH 21/31] code tidy ups --- API/MqttService.cs | 63 +++++++++++++++++++++------------------------- MainWindow.xaml.cs | 6 ++--- TEAMS2HA.csproj | 4 +-- 3 files changed, 33 insertions(+), 40 deletions(-) diff --git a/API/MqttService.cs b/API/MqttService.cs index 25e4fab..7090704 100644 --- a/API/MqttService.cs +++ b/API/MqttService.cs @@ -3,18 +3,12 @@ using MQTTnet.Protocol; using Serilog; using System; -using System.Security.Cryptography.X509Certificates; 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; using System.Timers; using Newtonsoft.Json; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.ListView; using System.Text; namespace TEAMS2HA.API @@ -28,15 +22,17 @@ public class MqttService private readonly string _deviceId; private bool _isAttemptingConnection = false; private MqttClient _mqttClient; + private bool _mqttClientsubscribed = false; private MqttClientOptions _mqttOptions; private Dictionary _previousSensorStates; private List _sensorNames; private AppSettings _settings; - private System.Timers.Timer mqttPublishTimer; private HashSet _subscribedTopics = new HashSet(); + private System.Timers.Timer mqttPublishTimer; private bool mqttPublishTimerset = false; + public delegate Task CommandToTeamsHandler(string jsonMessage); - private bool _mqttClientsubscribed = false; + public event CommandToTeamsHandler CommandToTeams; public event Action StatusUpdated; @@ -82,24 +78,7 @@ public bool IsAttemptingConnection #endregion Public Properties #region Public Methods - public async Task UpdateClientOptionsAndReconnect() - { - InitializeClientOptions(); // Method to reinitialize client options with updated settings - await DisconnectAsync(); - await ConnectAsync(); - } - - public async Task UpdateSettingsAsync(AppSettings newSettings) - { - _settings = newSettings; - InitializeClientOptions(); // Reinitialize MQTT client options - if (IsConnected) - { - await DisconnectAsync(); - await ConnectAsync(); - } - } public static List GetEntityNames(string deviceId) { var entityNames = new List @@ -197,7 +176,7 @@ public void Dispose() public void InitializeMqttPublishTimer() { mqttPublishTimer = new System.Timers.Timer(60000); // Set the interval to 60 seconds - if(mqttPublishTimerset == false) + if (mqttPublishTimerset == false) { mqttPublishTimer.Elapsed += OnMqttPublishTimerElapsed; mqttPublishTimer.AutoReset = true; // Reset the timer after it elapses @@ -395,17 +374,35 @@ public async Task SubscribeAsync(string topic, MqttQualityOfServiceLevel qos) } } + public async Task UpdateClientOptionsAndReconnect() + { + InitializeClientOptions(); // Method to reinitialize client options with updated settings + await DisconnectAsync(); + await ConnectAsync(); + } - public void UpdateConnectionStatus(string status) + public void UpdateConnectionStatus(string status) //could be obsolete { OnConnectionStatusChanged(status); } + public async Task UpdateSettingsAsync(AppSettings newSettings) + { + _settings = newSettings; + InitializeClientOptions(); // Reinitialize MQTT client options + + if (IsConnected) + { + await DisconnectAsync(); + await ConnectAsync(); + } + } + #endregion Public Methods #region Protected Methods - protected virtual void OnConnectionStatusChanged(string status) + protected virtual void OnConnectionStatusChanged(string status) //could be obsolete { ConnectionStatusChanged?.Invoke(status); } @@ -414,8 +411,6 @@ protected virtual void OnConnectionStatusChanged(string status) #region Private Methods - - private string DetermineDeviceClass(string sensor) { switch (sensor) @@ -561,12 +556,11 @@ private void InitializeClient() _mqttClient = (MqttClient?)factory.CreateMqttClient(); // This creates an IMqttClient, not a MqttClient. InitializeClientOptions(); // Ensure options are initialized with current settings - if(_mqttClientsubscribed == false) + if (_mqttClientsubscribed == false) { _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; _mqttClientsubscribed = true; } - } } @@ -637,12 +631,11 @@ private void InitializeClientOptions() _mqttOptions = mqttClientOptionsBuilder.Build(); if (_mqttClient != null) { - if(_mqttClientsubscribed == false) + if (_mqttClientsubscribed == false) { _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; _mqttClientsubscribed = true; } - } } catch (Exception ex) @@ -669,7 +662,7 @@ private Task OnMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs e) string command = payload; // command should be ON or OFF based on the payload // Now call the handle method - HandleSwitchCommand(topic, command); + HandleSwitchCommand(topic, command); } return Task.CompletedTask; diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 0ae8f04..cb2f6ea 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -477,14 +477,14 @@ private bool CheckIfSensorPrefixChanged(AppSettings newSettings) return newSettings.SensorPrefix != currentSettings.SensorPrefix; } - private async void CheckMqttConnection() + private async void CheckMqttConnection() //could be obsolete { if (_mqttService != null && !_mqttService.IsConnected && !_mqttService.IsAttemptingConnection) { Log.Debug("CheckMqttConnection: MQTT Client Not Connected. Attempting reconnection."); await _mqttService.ConnectAsync(); await _mqttService.SubscribeAsync("homeassistant/switch/+/set", MqttQualityOfServiceLevel.AtLeastOnce); - //UpdateConnectionStatus(); + _mqttService.UpdateConnectionStatus("Connected"); UpdateStatusMenuItems(); } } @@ -718,7 +718,7 @@ private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e) } } - private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) + private void OnTimedEvent(Object source, System.Timers.ElapsedEventArgs e) //obsolete? { // Check the MQTT connection CheckMqttConnection(); diff --git a/TEAMS2HA.csproj b/TEAMS2HA.csproj index b09243c..fba7462 100644 --- a/TEAMS2HA.csproj +++ b/TEAMS2HA.csproj @@ -5,8 +5,8 @@ net7.0-windows enable true - 1.1.0.462 - 1.1.0.462 + 1.1.0.465 + 1.1.0.465 Assets\Square150x150Logo.scale-200.ico Teams2HA Square150x150Logo.scale-200.png From f22f4405327cf7e631048b274858e4027880ea8c Mon Sep 17 00:00:00 2001 From: Jimmy White Date: Tue, 12 Mar 2024 15:21:25 +0000 Subject: [PATCH 22/31] UI updates --- API/MqttService.cs | 18 ++++++++++-------- AboutWindow.xaml | 14 +++++++++----- App.xaml | 4 ++++ Assets/appbg.png | Bin 0 -> 467234 bytes MainWindow.xaml | 44 ++++++++++++++++++++++++++------------------ TEAMS2HA.csproj | 9 +++++++-- appbg.png | Bin 0 -> 467234 bytes 7 files changed, 56 insertions(+), 33 deletions(-) create mode 100644 Assets/appbg.png create mode 100644 appbg.png diff --git a/API/MqttService.cs b/API/MqttService.cs index 7090704..619e28e 100644 --- a/API/MqttService.cs +++ b/API/MqttService.cs @@ -31,17 +31,10 @@ public class MqttService private System.Timers.Timer mqttPublishTimer; private bool mqttPublishTimerset = false; - public delegate Task CommandToTeamsHandler(string jsonMessage); - - public event CommandToTeamsHandler CommandToTeams; - - public event Action StatusUpdated; - #endregion Private Fields #region Public Constructors - // Constructor public MqttService(AppSettings settings, string deviceId, List sensorNames) { _settings = settings; @@ -55,14 +48,24 @@ public MqttService(AppSettings settings, string deviceId, List sensorNam #endregion Public Constructors + #region Public Delegates + + public delegate Task CommandToTeamsHandler(string jsonMessage); + + #endregion Public Delegates + #region Public Events + public event CommandToTeamsHandler CommandToTeams; + public event Action ConnectionAttempting; public event Action ConnectionStatusChanged; public event Func MessageReceived; + public event Action StatusUpdated; + #endregion Public Events #region Public Properties @@ -692,6 +695,5 @@ private void OnMqttPublishTimerElapsed(object sender, ElapsedEventArgs e) #endregion Private Methods - // Additional methods for sensor management, message handling, etc. } } \ No newline at end of file diff --git a/AboutWindow.xaml b/AboutWindow.xaml index df53825..956e232 100644 --- a/AboutWindow.xaml +++ b/AboutWindow.xaml @@ -5,7 +5,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" mc:Ignorable="d" - Title="About" Height="486" Width="400" + Title="About" Height="531" Width="400" WindowStartupLocation="CenterScreen" ResizeMode="NoResize" TextElement.Foreground="{DynamicResource MaterialDesignBody}" TextElement.FontWeight="Regular" @@ -27,8 +27,12 @@ - - + + + + + + @@ -38,7 +42,7 @@ - + @@ -53,7 +57,7 @@ -