diff --git a/API/Device.cs b/API/Device.cs new file mode 100644 index 0000000..f178c75 --- /dev/null +++ b/API/Device.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TEAMS2HA.API +{ + internal class Device + { + public string? Identifiers { get; set; } + public string? Name { get; set; } + public string? SwVersion { get; set; } + public string? Model { get; set; } + public string? Manufacturer { get; set; } + } +} diff --git a/API/Homeassistant.cs b/API/Homeassistant.cs deleted file mode 100644 index 4ce278f..0000000 --- a/API/Homeassistant.cs +++ /dev/null @@ -1,470 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - - -namespace TEAMS2HA.API -{ - public class HomeAssistant - { - #region Private Fields - - private string _apiKey; - private string _baseUrl; - private HttpClient _httpClient; - private bool connected = false; - private bool isEnabled = false; - private string name = "Homeassistant"; - private string oldact = null; - private string oldcam = null; - private string oldmic = null; - private string oldstattus = null; - private TEAMS2HA.API.State stateInstance; - private readonly ClientWebSocket _webSocket = new ClientWebSocket(); - private readonly Uri _url; - private readonly string _accessToken; - private int _requestIdSequence = 1; - private Dictionary> _requests = new Dictionary>(); - - - #endregion Private Fields - - #region Public Constructors - - public HomeAssistant(string baseUrl, string apiKey) - { - _baseUrl = baseUrl; - _apiKey = apiKey; - _httpClient = new HttpClient(); - _httpClient.DefaultRequestHeaders.Add("Authorization", _apiKey); - _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey); - stateInstance = TEAMS2HA.API.State.Instance; - stateInstance.StateChanged += OnStateChanged; - - } - - #endregion Public Constructors - - #region Public Events - - public event EventHandler StateChanged; - - #endregion Public Events - - #region Public Properties - - public string Name - { - get { return name; } - } - - public string State - { - get { return stateInstance.ToString(); } - set { /* You can leave this empty since the State property is read-only */ } - } - - #endregion Public Properties - - #region Public Methods - - private Dictionary activityIcons = new Dictionary() - { - { "In a call", "mdi:phone-in-talk-outline" }, - { "On the phone", "mdi:phone-in-talk-outline" }, - { "Offline", "mdi:account-off" }, - { "In a meeting", "mdi:phone-in-talk-outline" }, - { "In A Conference Call", "mdi:phone-in-talk-outline" }, - { "Out of Office", "mdi:account-off" }, - { "Not in a Call", "mdi:account" }, - { "Presenting", "mdi:presentation-play" } - }; - - private Dictionary statusIcons = new Dictionary() - { - { "Busy", "mdi:account-cancel" }, - { "On the phone", "mdi:phone-in-talk-outline" }, - { "Do not disturb", "mdi:minus-circle-outline" }, - { "Away", "mdi:timer-sand" }, - { "Be right back", "mdi:timer-sand" }, - { "Available", "mdi:account" }, - { "Offline", "mdi:account-off" } - }; - - public static bool IsValidUrl(string url) - { - Uri uriResult; - return Uri.TryCreate(url, UriKind.Absolute, out uriResult) - && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps); - } - - public async Task CheckConnectionAsync() - { - try - { - var response = await _httpClient.GetAsync($"{_baseUrl}/api/states"); - - if (response.IsSuccessStatusCode) - { - connected = true; - } - else - { - connected = false; - } - return response.IsSuccessStatusCode; - } - catch (Exception) - { - return false; - } - } - - public async Task Create_Entity(string entity) - { - if (!IsValidUrl(_baseUrl)) - { return; } - var client = new HttpClient(); - client.BaseAddress = new Uri(_baseUrl + "/api/"); - - var token = "Bearer " + _apiKey; - client.DefaultRequestHeaders.Add("Authorization", token); - - var content = new StringContent($@"{{ - ""entity_id"": ""{entity}"", - ""state"": ""Unknown"" - }}"); - - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - - try - { - var response = await client.PostAsync($"states/{entity}", content); - response.EnsureSuccessStatusCode(); - } - catch (Exception ex) - { - } - client.Dispose(); - } - - public async Task EntityExists(string entity) - { - if (!IsValidUrl(_baseUrl)) - { return false; } - var client = new HttpClient(); - client.BaseAddress = new Uri(_baseUrl + "/api/"); - client.DefaultRequestHeaders.Add("Authorization", "Bearer " + _apiKey); - try - { - var response = await client.GetAsync($"states/{entity}"); - return response.IsSuccessStatusCode; - client.Dispose(); - } - catch - { - return false; - } - } - - public async Task Start() - { - var connectionSuccessful = await CheckConnectionAsync(); - if (connectionSuccessful) - { - if (string.IsNullOrWhiteSpace(_baseUrl)) - { - return; - } - - string[] entityNames = new string[] - { - "sensor.hats_isSharing", - "sensor.hats_activity", - "switch.hats_camera", - "switch.hats_microphone", - "switch.hats_blurred", - "switch.hats_hand", - "switch.hats_recording" - }; - - foreach (var entityName in entityNames) - { - if (!await EntityExists(entityName)) - { - await Create_Entity(entityName); - } - } - string _wsUrl; - - if (_baseUrl.StartsWith("https://")) - { - _wsUrl = _baseUrl.Replace("https://", "wss://") + "/api/websocket"; - } - else if (_baseUrl.StartsWith("http://")) - { - _wsUrl = _baseUrl.Replace("http://", "ws://") + "/api/websocket"; - } - else - { - throw new InvalidOperationException("Invalid _baseUrl format. Should start with http:// or https://"); - } - - var client = new HomeAssistantClient(_wsUrl, _apiKey); - await client.ConnectAsync(); - - } - } - - public async Task UpdateEntity(string entityName, string stateText, string icon) - { - if (connected) - { - var client = new HttpClient(); - client.BaseAddress = new Uri(_baseUrl + "/api/"); - var token = "Bearer " + _apiKey; - client.DefaultRequestHeaders.Add("Authorization", token); - System.Net.Http.HttpResponseMessage response; - - try - { - response = await client.GetAsync($"states/{entityName}"); - //Log.Debug("Response to GET request: {response}", response.ReasonPhrase); - } - catch (Exception ex) - { - client.Dispose(); - isEnabled = false; - return; - } - //Log.Debug("Response to GET request: {response}", response); - - if (response.IsSuccessStatusCode) - { - //Log.Information("Updating {entity} in Home assistant to {state}", entityName, stateText); - - var payload = $@"{{ - ""state"": ""{stateText}"", - ""entity_id"": ""{entityName}"", - ""attributes"": {{ - ""icon"": ""{icon}"" - }} - }}"; - - var content = new StringContent(payload); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - //Log.Debug("Attempting to set homeassistant "+content.ToString()); - try - { - response = await client.PostAsync($"states/{entityName}", content); - } - catch (Exception ex) - { - client.Dispose(); - } - //Log.Debug("Response to POST request: {response}", response.ReasonPhrase); - } - else - { - client.Dispose(); - isEnabled = false; - return; - } - - response.EnsureSuccessStatusCode(); - client.Dispose(); - } - } - - private async void OnStateChanged(object sender, EventArgs e) - { - if (connected) - { - stateInstance = (State)sender; - StateChanged?.Invoke(this, EventArgs.Empty); - - string statusIcon = statusIcons.TryGetValue(stateInstance.Status, out var icon) ? icon : "mdi:presentation-play"; - string activityIcon = activityIcons.TryGetValue(stateInstance.Activity, out _) ? icon : "mdi:account-off"; - - if (stateInstance.Status != oldstattus) - await UpdateEntity("sensor.hats_isSharing", stateInstance.Status, statusIcon); - - if (stateInstance.Activity != oldact) - await UpdateEntity("sensor.hats_activity", stateInstance.Activity, activityIcon); - await UpdateEntity("sensor.hats_isSharing", stateInstance.issharing, statusIcon); - await UpdateEntity("switch.hats_camera", stateInstance.Camera, stateInstance.Camera == "On" ? "mdi:camera" : "mdi:camera-off"); - await UpdateEntity("switch.hats_microphone", stateInstance.Microphone, stateInstance.Microphone == "Off" ? "mdi:microphone" : "mdi:microphone-off"); - await UpdateEntity("switch.hats_blurred", stateInstance.Blurred, stateInstance.Blurred == "Blurred" ? "mdi:blur" : "mdi:blur-off"); - await UpdateEntity("switch.hats_hand", stateInstance.Handup, stateInstance.Handup == "Raised" ? "mdi:hand-back-left" : "mdi:hand-back-left-off"); - await UpdateEntity("switch.hats_recording", stateInstance.Recording, stateInstance.Recording == "On" ? "mdi:record-circle" : "mdi:record"); - - oldstattus = stateInstance.Status; - oldact = stateInstance.Activity; - } - } - } - - public class HomeAssistantClient - { - private readonly ClientWebSocket _webSocket = new ClientWebSocket(); - private readonly Uri _url; - private readonly string _accessToken; - private int _requestIdSequence = 1; - private Dictionary> _requests = new Dictionary>(); - private string _apiKey; - private string _baseUrl; - - public HomeAssistantClient(string url, string accessToken) - { - _baseUrl = url; - _apiKey = accessToken; - _url = new Uri(url); - _accessToken = accessToken; - - } - - - public async Task ConnectAsync() - { - try - { - await _webSocket.ConnectAsync(_url, CancellationToken.None); - _ = ProcessMessagesAsync(); - } - catch (Exception ex) - { - Debug.WriteLine("Exception during connection: " + ex.Message); - } - } - - public async Task SubscribeToEventsAsync(Action callback) - { - string[] entityNames = new string[] - { - "sensor.hats_status", - "sensor.hats_activity", - "switch.hats_camera", - "switch.hats_microphone", - "switch.hats_blurred", - "switch.hats_hand", - "switch.hats_recording" - }; - - var requestId = _requestIdSequence++; - _requests[requestId] = eventData => - { - if (eventData["data"] is JObject data && data["entity_id"] is JValue entityId && entityNames.Contains((string)entityId)) - { - callback(eventData); - } - }; - - var command = new - { - id = requestId, - type = "subscribe_events", - event_type = "state_changed" - }; - - var commandJson = JObject.FromObject(command).ToString(); - await _webSocket.SendAsync(Encoding.UTF8.GetBytes(commandJson), WebSocketMessageType.Text, true, CancellationToken.None); - } - - private async Task ProcessMessagesAsync() - { - var buffer = new byte[4096]; - var completeMessage = new StringBuilder(); - - try - { - while (_webSocket.State == WebSocketState.Open) - { - WebSocketReceiveResult result; - do - { - result = await _webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - var messagePart = Encoding.UTF8.GetString(buffer, 0, result.Count); - completeMessage.Append(messagePart); - } - while (!result.EndOfMessage); - - var fullMessage = completeMessage.ToString(); - completeMessage.Clear(); - - if (result.MessageType == WebSocketMessageType.Close) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); - } - else - { - try - { - var messageData = JObject.Parse(fullMessage); - - switch ((string)messageData["type"]) - { - case "auth_required": - await SendAuthenticationAsync(); - break; - case "auth_ok": - Debug.WriteLine("Authenticated successfully"); - await SubscribeToEventsAsync(eventData => - { - Debug.WriteLine("Received event: " + eventData); - }); - break; - case "auth_invalid": - case "auth_failed": - Debug.WriteLine("Authentication failed: " + messageData["message"]); - break; - case "result": - case "event": - var id = (int)messageData["id"]; - if (_requests.TryGetValue(id, out var callback) && (messageData["event"] is JObject || messageData["result"] is JObject)) - { - callback(messageData["event"] ?? messageData["result"]); - } - break; - } - } - catch (JsonReaderException ex) - { - Debug.WriteLine($"Error parsing JSON: {ex.Message}"); - } - } - } - } - catch (WebSocketException ex) - { - Debug.WriteLine("WebSocketException: " + ex.Message); - } - catch (Exception ex) - { - Debug.WriteLine("Exception: " + ex.Message); - } - } - - - private async Task SendAuthenticationAsync() - { - var authMessage = new - { - type = "auth", - access_token = _accessToken - }; - - var authMessageJson = JObject.FromObject(authMessage).ToString(); - await _webSocket.SendAsync(Encoding.UTF8.GetBytes(authMessageJson), WebSocketMessageType.Text, true, CancellationToken.None); - } - #endregion Private Methods - } -} diff --git a/API/MqttClientWrapper.cs b/API/MqttClientWrapper.cs new file mode 100644 index 0000000..394bd5c --- /dev/null +++ b/API/MqttClientWrapper.cs @@ -0,0 +1,195 @@ +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Protocol; +using Serilog; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; + +namespace TEAMS2HA.API +{ + public class MqttClientWrapper + { + #region Private Fields + + private MqttClient _mqttClient; + private MqttClientOptions _mqttOptions; + + #endregion Private Fields + + #region Public Constructors + + public MqttClientWrapper(string clientId, string mqttBroker, string username, string password) + { + var factory = new MqttFactory(); + _mqttClient = factory.CreateMqttClient() as MqttClient; + + _mqttOptions = new MqttClientOptionsBuilder() + .WithClientId(clientId) + .WithTcpServer(mqttBroker) + .WithCredentials(username, password) + .WithCleanSession() + .Build(); + + _mqttClient.ApplicationMessageReceivedAsync += OnMessageReceivedAsync; + Log.Information("MQTT Client Created"); + } + + public MqttClientWrapper(/* parameters */) + { + // Existing initialization code... + + _mqttClient.ApplicationMessageReceivedAsync += HandleReceivedApplicationMessage; + Log.Information("MQTT Client Created"); + } + + #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 async Task ConnectAsync() + { + if (_mqttClient.IsConnected) + { + Log.Information("MQTT client is already connected."); + return; + } + + try + { + Log.Information("attempting to connect to mqtt"); + await _mqttClient.ConnectAsync(_mqttOptions); + + Log.Information("Connected to MQTT broker."); + } + catch (Exception ex) + { + Log.Debug($"Failed to connect to MQTT broker: {ex.Message}"); + } + } + + public async Task DisconnectAsync() + { + if (!_mqttClient.IsConnected) + { + Log.Debug("MQTTClient is not connected"); + return; + } + + try + { + await _mqttClient.DisconnectAsync(); + Log.Information("MQTT Disconnected"); + } + 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 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" + // Add more entities based on your application's functionality + }; + + return entityNames; + } + public async Task PublishAsync(string topic, string payload, bool retain = false) + { + try + { + Log.Information($"Publishing to topic: {topic}"); + Log.Information($"Payload: {payload}"); + Log.Information($"Retain flag: {retain}"); + + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce) + .WithRetainFlag(retain) + .Build(); + + await _mqttClient.PublishAsync(message); + Log.Information("Publish successful."); + } + catch (Exception ex) + { + 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); + } + + #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/Teams.cs b/API/Teams.cs index eeb0af9..c41a787 100644 --- a/API/Teams.cs +++ b/API/Teams.cs @@ -1,29 +1,41 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Serilog; using System; using System.Collections.Generic; +using System.IO; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; -using System.IO; -using System.Diagnostics; -using System.Windows.Threading; using System.Windows; - +using TEAMS2HA.Properties; namespace TEAMS2HA.API { public class WebSocketClient { #region Private Fields - private AppSettings _appSettings; - private readonly ClientWebSocket _clientWebSocket; - private readonly State _state; + + private ClientWebSocket _clientWebSocket; private readonly string _settingsFilePath; - private bool _isConnected; - + private readonly State _state; private readonly Action _updateTokenAction; + private bool _isConnected; + private Uri _currentUri; + + private TaskCompletionSource _pairingResponseTaskSource; + + private Dictionary meetingState = new Dictionary() + { + { "isMuted", false }, + { "isCameraOn", false }, + { "isHandRaised", false }, + { "isInMeeting", "Not in a meeting" }, + { "isRecordingOn", false }, + { "isBackgroundBlurred", false }, + }; + #endregion Private Fields #region Public Constructors @@ -33,16 +45,28 @@ public WebSocketClient(Uri uri, State state, string settingsFilePath, Action ConnectAsync(uri)); + // Task.Run(() => ConnectAsync(uri)); + Log.Debug("Websocket Client Started"); // Subscribe to the MessageReceived event MessageReceived += OnMessageReceived; + _updateTokenAction = updateTokenAction; } + #endregion Public Constructors + + #region Public Events + public event Action ConnectionStatusChanged; + public event EventHandler MessageReceived; + + public event EventHandler TeamsUpdateReceived; + + #endregion Public Events + + #region Public Properties + public bool IsConnected { get => _isConnected; @@ -52,213 +76,279 @@ private set { _isConnected = value; ConnectionStatusChanged?.Invoke(_isConnected); + Log.Debug($"Connection Status Changed: {_isConnected}"); } } } - public async Task StopAsync(CancellationToken cancellationToken = default) - { - MessageReceived -= OnMessageReceived; - if (_clientWebSocket.State == WebSocketState.Open) - { - await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by client", cancellationToken); - Console.WriteLine("WebSocket connection closed"); - } - } - - #endregion Public Constructors - - #region Public Events - - public event EventHandler MessageReceived; - - #endregion Public Events + #endregion Public Properties #region Public Methods - public async Task SendMessageAsync(string message, CancellationToken cancellationToken = default) - { - byte[] messageBytes = Encoding.UTF8.GetBytes(message); - await _clientWebSocket.SendAsync(new ArraySegment(messageBytes), WebSocketMessageType.Text, true, cancellationToken); - Console.WriteLine($"Message sent: {message}"); - } - - #endregion Public Methods - - #region Private Methods - - private Dictionary meetingState = new Dictionary() - { - { "isMuted", false }, - { "isCameraOn", false }, - { "isHandRaised", false }, - { "isInMeeting", "Not in a meeting" }, - { "isRecordingOn", false }, - { "isBackgroundBlurred", false }, - }; - - private async Task ConnectAsync(Uri uri) + public async Task ConnectAsync(Uri uri) { + _currentUri = uri; try { - string token = _appSettings.TeamsToken; + // Check if the WebSocket is already connecting or connected + if (_clientWebSocket.State != WebSocketState.None && + _clientWebSocket.State != WebSocketState.Closed) + { + Log.Debug("ConnectAsync: WebSocket is already connecting or connected."); + return; + } + + string token = AppSettings.Instance.PlainTeamsToken; 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; } await _clientWebSocket.ConnectAsync(uri, CancellationToken.None); - Console.WriteLine("WebSocket connected"); + Log.Debug($"Connected to {uri}"); IsConnected = _clientWebSocket.State == WebSocketState.Open; + Log.Debug($"IsConnected: {IsConnected}"); } catch (Exception ex) { IsConnected = false; - Console.WriteLine($"Connection failed: {ex.Message}"); - // Other error handling... + Log.Error(ex, "ConnectAsync: Error connecting to WebSocket"); + await ReconnectAsync(); } + // Start receiving messages await ReceiveLoopAsync(); } - private AppSettings LoadAppSettings(string filePath) + + public async Task PairWithTeamsAsync() { - if (File.Exists(filePath)) + if (_isConnected) { - string json = File.ReadAllText(filePath); - return JsonConvert.DeserializeObject(json); + _pairingResponseTaskSource = new TaskCompletionSource(); + + string pairingCommand = "{\"action\":\"pair\",\"parameters\":{},\"requestId\":1}"; + await SendMessageAsync(pairingCommand); + + var responseTask = await Task.WhenAny(_pairingResponseTaskSource.Task, Task.Delay(TimeSpan.FromSeconds(30))); + + if (responseTask == _pairingResponseTaskSource.Task) + { + var response = await _pairingResponseTaskSource.Task; + var newToken = JsonConvert.DeserializeObject(response).NewToken; + AppSettings.Instance.PlainTeamsToken = newToken; + AppSettings.Instance.SaveSettingsToFile(); + + _updateTokenAction?.Invoke(newToken); // Invoke the action to update UI + //subscribe to meeting updates + } + else + { + Log.Warning("Pairing response timed out."); + } } - else + } + + public async Task SendMessageAsync(string message, CancellationToken cancellationToken = default) + { + byte[] messageBytes = Encoding.UTF8.GetBytes(message); + await _clientWebSocket.SendAsync(new ArraySegment(messageBytes), WebSocketMessageType.Text, true, cancellationToken); + Log.Debug($"Message Sent: {message}"); + } + + // Public method to initiate connection + public async Task StartConnectionAsync(Uri uri) + { + if (!_isConnected && _clientWebSocket.State != WebSocketState.Open) { - return new AppSettings(); // Defaults if file does not exist + await ConnectAsync(uri); } } - private void SaveAppSettings(AppSettings settings) + + public async Task StopAsync(CancellationToken cancellationToken = default) { - string json = JsonConvert.SerializeObject(settings, Formatting.Indented); - File.WriteAllText(_settingsFilePath, json); + MessageReceived -= OnMessageReceived; + if (_clientWebSocket.State == WebSocketState.Open) + { + await _clientWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by client", cancellationToken); + + Log.Debug("Websocket Connection Closed"); + } } - private void OnMessageReceived(object sender, string message) + + #endregion Public Methods + + #region Private Methods + + private bool IsPairingResponse(string message) { + // Implement logic to determine if the message is a response to the pairing request This + // could be based on message content, format, etc. + return message.Contains("tokenRefresh"); + } - Debug.WriteLine($"Message received: {message}"); // Add this line + private void OnMessageReceived(object sender, string message) + { + Log.Debug($"Message Received: {message}"); if (message.Contains("tokenRefresh")) { + _pairingResponseTaskSource?.SetResult(message); + Log.Information("Result Message {message}", message); var tokenUpdate = JsonConvert.DeserializeObject(message); - _appSettings.TeamsToken = tokenUpdate.NewToken; - SaveAppSettings(_appSettings); - + AppSettings.Instance.PlainTeamsToken = tokenUpdate.NewToken; + AppSettings.Instance.SaveSettingsToFile(); + Log.Debug($"Token Updated: {AppSettings.Instance.PlainTeamsToken}"); // Update the UI on the main thread Application.Current.Dispatcher.Invoke(() => { - _updateTokenAction?.Invoke(_appSettings.TeamsToken); + _updateTokenAction?.Invoke(AppSettings.Instance.PlainTeamsToken); }); } - // Update the Message property of the State class - var settings = new JsonSerializerSettings + else if (message.Contains("meetingPermissions")) // Replace with actual keyword/structure { - Converters = new List { new MeetingUpdateConverter() } - }; - - MeetingUpdate meetingUpdate = JsonConvert.DeserializeObject(message, settings); + Log.Debug("Pairing..."); + // Update UI, save settings, reinitialize connection as needed - if (meetingUpdate?.MeetingPermissions?.CanPair == true) - { - // The 'canPair' permission is true, initiate pairing - PairWithTeamsAsync(); - } - // Update the meeting state dictionary - if (meetingUpdate.MeetingState != null) - { - meetingState["isMuted"] = meetingUpdate.MeetingState.IsMuted; - meetingState["isCameraOn"] = meetingUpdate.MeetingState.IsVideoOn; - meetingState["isHandRaised"] = meetingUpdate.MeetingState.IsHandRaised; - meetingState["isInMeeting"] = meetingUpdate.MeetingState.IsInMeeting; - meetingState["isRecordingOn"] = meetingUpdate.MeetingState.IsRecordingOn; - meetingState["isBackgroundBlurred"] = meetingUpdate.MeetingState.IsBackgroundBlurred; - meetingState["isSharing"] = meetingUpdate.MeetingState.IsSharing; - if (meetingUpdate.MeetingState.IsVideoOn) + // Update the Message property of the State class + var settings = new JsonSerializerSettings { - State.Instance.Camera = "On"; - } - else - { - State.Instance.Camera = "Off"; - } + Converters = new List { new MeetingUpdateConverter() } + }; - if (meetingUpdate.MeetingState.IsInMeeting) - { - State.Instance.Activity = "In a meeting"; - } - else - { - State.Instance.Activity = "Not in a Call"; - } + MeetingUpdate meetingUpdate = JsonConvert.DeserializeObject(message, settings); - if (meetingUpdate.MeetingState.IsMuted) + if (meetingUpdate?.MeetingPermissions?.CanPair == true) { - State.Instance.Microphone = "On"; + // The 'canPair' permission is true, initiate pairing + Log.Debug("Pairing with Teams"); + PairWithTeamsAsync(); } - else + // Update the meeting state dictionary + if (meetingUpdate.MeetingState != null) { - State.Instance.Microphone = "Off"; - } + meetingState["isMuted"] = meetingUpdate.MeetingState.IsMuted; + meetingState["isCameraOn"] = meetingUpdate.MeetingState.IsVideoOn; + meetingState["isHandRaised"] = meetingUpdate.MeetingState.IsHandRaised; + meetingState["isInMeeting"] = meetingUpdate.MeetingState.IsInMeeting; + meetingState["isRecordingOn"] = meetingUpdate.MeetingState.IsRecordingOn; + meetingState["isBackgroundBlurred"] = meetingUpdate.MeetingState.IsBackgroundBlurred; + meetingState["isSharing"] = meetingUpdate.MeetingState.IsSharing; + if (meetingUpdate.MeetingState.IsVideoOn) + { + State.Instance.Camera = "On"; + } + else + { + State.Instance.Camera = "Off"; + } - if (meetingUpdate.MeetingState.IsHandRaised) - { - State.Instance.Handup = "Raised"; - } - else - { - State.Instance.Handup = "Lowered"; - } + if (meetingUpdate.MeetingState.IsInMeeting) + { + State.Instance.Activity = "In a meeting"; + } + else + { + State.Instance.Activity = "Not in a Call"; + } - if (meetingUpdate.MeetingState.IsRecordingOn) - { - State.Instance.Recording = "On"; - } - else - { - State.Instance.Recording = "Off"; - } + if (meetingUpdate.MeetingState.IsMuted) + { + State.Instance.Microphone = "On"; + } + else + { + State.Instance.Microphone = "Off"; + } - if (meetingUpdate.MeetingState.IsBackgroundBlurred) - { - State.Instance.Blurred = "Blurred"; - } - else - { - State.Instance.Blurred = "Not Blurred"; + if (meetingUpdate.MeetingState.IsHandRaised) + { + State.Instance.Handup = "Raised"; + } + else + { + State.Instance.Handup = "Lowered"; + } + + if (meetingUpdate.MeetingState.IsRecordingOn) + { + State.Instance.Recording = "On"; + } + else + { + State.Instance.Recording = "Off"; + } + + if (meetingUpdate.MeetingState.IsBackgroundBlurred) + { + State.Instance.Blurred = "Blurred"; + } + else + { + State.Instance.Blurred = "Not Blurred"; + } + if (meetingUpdate.MeetingState.IsSharing) + { + State.Instance.issharing = "Sharing"; + } + else + { + State.Instance.issharing = "Not Sharing"; + } + try + { + TeamsUpdateReceived?.Invoke(this, new TeamsUpdateEventArgs { MeetingUpdate = meetingUpdate }); + } + catch (Exception ex) + { + Log.Error(ex, "Error in TeamsUpdateReceived"); + } + Log.Debug($"Meeting State Updated: {meetingState}"); } - if (meetingUpdate.MeetingState.IsSharing) + } + } + private async Task ReconnectAsync() + { + const int maxRetryCount = 5; + int retryDelay = 2000; // milliseconds + int retryCount = 0; + + while (retryCount < maxRetryCount) + { + try { - State.Instance.issharing = "Sharing"; + Log.Debug($"Attempting reconnection, try {retryCount + 1} of {maxRetryCount}"); + _clientWebSocket = new ClientWebSocket(); // Create a new instance + await ConnectAsync(_currentUri); + if (IsConnected) + { + break; + } } - else + catch (Exception ex) { - State.Instance.issharing = "Not Sharing"; + Log.Error($"Reconnect attempt {retryCount + 1} failed: {ex.Message}"); } - // need to edit state class to add handraised, recording, and backgroundblur + retryCount++; + await Task.Delay(retryDelay); } - } - private async Task PairWithTeamsAsync() - { - if (_isConnected) + if (IsConnected) { - string pairingCommand = "{\"action\":\"pair\",\"parameters\":{},\"requestId\":1}"; - await SendMessageAsync(pairingCommand); - - // Handle the response here - // For example: Check if the response contains a success message or token + Log.Information("Reconnected successfully."); + } + else + { + Log.Warning("Failed to reconnect after several attempts."); } } + private async Task ReceiveLoopAsync(CancellationToken cancellationToken = default) { const int bufferSize = 4096; // Starting buffer size @@ -267,30 +357,57 @@ private async Task ReceiveLoopAsync(CancellationToken cancellationToken = defaul while (_clientWebSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) { - WebSocketReceiveResult result = await _clientWebSocket.ReceiveAsync(new ArraySegment(buffer, totalBytesReceived, buffer.Length - totalBytesReceived), cancellationToken); - totalBytesReceived += result.Count; - - if (result.EndOfMessage) + try { - string messageReceived = Encoding.UTF8.GetString(buffer, 0, totalBytesReceived); - Console.WriteLine($"Message received: {messageReceived}"); - - if (!cancellationToken.IsCancellationRequested && !string.IsNullOrEmpty(messageReceived)) + WebSocketReceiveResult result = await _clientWebSocket.ReceiveAsync(new ArraySegment(buffer, totalBytesReceived, buffer.Length - totalBytesReceived), cancellationToken); + totalBytesReceived += result.Count; + if (result.CloseStatus.HasValue) { - MessageReceived?.Invoke(this, messageReceived); + Log.Debug($"WebSocket closed with status: {result.CloseStatus}"); + IsConnected = false; + break; // Exit the loop if the WebSocket is closed } + if (result.EndOfMessage) + { + string messageReceived = Encoding.UTF8.GetString(buffer, 0, totalBytesReceived); + Log.Debug($"ReceiveLoopAsync: Message Received: {messageReceived}"); + + if (!cancellationToken.IsCancellationRequested && !string.IsNullOrEmpty(messageReceived)) + { + MessageReceived?.Invoke(this, messageReceived); + } - // Reset buffer and totalBytesReceived for next message - buffer = new byte[bufferSize]; - totalBytesReceived = 0; + // Reset buffer and totalBytesReceived for next message + buffer = new byte[bufferSize]; + totalBytesReceived = 0; + } + else if (totalBytesReceived == buffer.Length) // Resize buffer if it's too small + { + Array.Resize(ref buffer, buffer.Length + bufferSize); + } } - else if (totalBytesReceived == buffer.Length) // Resize buffer if it's too small + catch (Exception ex) { - Array.Resize(ref buffer, buffer.Length + bufferSize); + Log.Error($"WebSocketException in ReceiveLoopAsync: {ex.Message}"); + IsConnected = false; + await ReconnectAsync(); + break; } } IsConnected = _clientWebSocket.State == WebSocketState.Open; + Log.Debug($"IsConnected: {IsConnected}"); } + + + private void SaveAppSettings(AppSettings settings) + { + string json = JsonConvert.SerializeObject(settings, Formatting.Indented); + File.WriteAllText(_settingsFilePath, json); + } + + #endregion Private Methods + + #region Public Classes public class MeetingUpdateConverter : JsonConverter { @@ -336,6 +453,15 @@ public override void WriteJson(JsonWriter writer, MeetingUpdate value, JsonSeria #endregion Public Methods } + public class TeamsUpdateEventArgs : EventArgs + { + #region Public Properties + + public MeetingUpdate MeetingUpdate { get; set; } + + #endregion Public Properties + } + public class TokenUpdate { #region Public Properties @@ -346,6 +472,6 @@ public class TokenUpdate #endregion Public Properties } - #endregion Private Methods + #endregion Public Classes } } \ No newline at end of file diff --git a/API/TokenStorage.cs b/API/TokenStorage.cs deleted file mode 100644 index 5658bea..0000000 --- a/API/TokenStorage.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; - -namespace TEAMS2HA.API -{ - public static class TokenStorage - { - public static void SaveHomeassistantToken(string token) - { - SaveToken(token, "HomeassistantToken"); - } - - public static string GetHomeassistantToken() - { - return GetToken("HomeassistantToken"); - } - - public static void SaveTeamsToken(string token) - { - SaveToken(token, "TeamsToken"); - } - - public static string GetTeamsToken() - { - return GetToken("TeamsToken"); - } - - private static void SaveToken(string token, string settingKey) - { - if (!string.IsNullOrEmpty(token)) - { - byte[] encryptedToken = ProtectedData.Protect( - Encoding.UTF8.GetBytes(token), - null, - DataProtectionScope.CurrentUser); - Properties.Settings.Default[settingKey] = Convert.ToBase64String(encryptedToken); - Properties.Settings.Default.Save(); - } - } - - private static string GetToken(string settingKey) - { - string encryptedTokenBase64 = Properties.Settings.Default[settingKey] as string; - if (!string.IsNullOrEmpty(encryptedTokenBase64)) - { - byte[] encryptedToken = Convert.FromBase64String(encryptedTokenBase64); - byte[] decryptedToken = ProtectedData.Unprotect( - encryptedToken, - null, - DataProtectionScope.CurrentUser); - return Encoding.UTF8.GetString(decryptedToken); - } - return null; - } - } -} diff --git a/AboutWindow.xaml b/AboutWindow.xaml new file mode 100644 index 0000000..df53825 --- /dev/null +++ b/AboutWindow.xaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +