diff --git a/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.Designer.cs b/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.Designer.cs index f09d5b9..303350e 100644 --- a/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.Designer.cs +++ b/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.Designer.cs @@ -51,6 +51,9 @@ private void InitializeComponent() this.label8 = new System.Windows.Forms.Label(); this.label7 = new System.Windows.Forms.Label(); this.label6 = new System.Windows.Forms.Label(); + this.label1 = new System.Windows.Forms.Label(); + this.TbMqttClientId = new System.Windows.Forms.TextBox(); + this.label2 = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)(this.TbIntMqttPort)).BeginInit(); this.SuspendLayout(); // @@ -136,7 +139,7 @@ private void InitializeComponent() this.BtnMqttClearConfig.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); this.BtnMqttClearConfig.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.BtnMqttClearConfig.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnMqttClearConfig.Location = new System.Drawing.Point(450, 409); + this.BtnMqttClearConfig.Location = new System.Drawing.Point(443, 438); this.BtnMqttClearConfig.Name = "BtnMqttClearConfig"; this.BtnMqttClearConfig.Size = new System.Drawing.Size(228, 31); this.BtnMqttClearConfig.Style.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); @@ -155,7 +158,7 @@ private void InitializeComponent() // this.label14.AutoSize = true; this.label14.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label14.Location = new System.Drawing.Point(42, 376); + this.label14.Location = new System.Drawing.Point(42, 370); this.label14.Name = "label14"; this.label14.Size = new System.Drawing.Size(135, 13); this.label14.TabIndex = 56; @@ -177,7 +180,8 @@ private void InitializeComponent() this.TbMqttDiscoveryPrefix.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; this.TbMqttDiscoveryPrefix.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.TbMqttDiscoveryPrefix.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.TbMqttDiscoveryPrefix.Location = new System.Drawing.Point(45, 348); + this.TbMqttDiscoveryPrefix.Location = new System.Drawing.Point(45, 342); + this.TbMqttDiscoveryPrefix.MaxLength = 100; this.TbMqttDiscoveryPrefix.Name = "TbMqttDiscoveryPrefix"; this.TbMqttDiscoveryPrefix.Size = new System.Drawing.Size(191, 25); this.TbMqttDiscoveryPrefix.TabIndex = 53; @@ -186,7 +190,7 @@ private void InitializeComponent() // this.label10.AutoSize = true; this.label10.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); - this.label10.Location = new System.Drawing.Point(44, 328); + this.label10.Location = new System.Drawing.Point(44, 322); this.label10.Name = "label10"; this.label10.Size = new System.Drawing.Size(100, 17); this.label10.TabIndex = 54; @@ -308,11 +312,46 @@ private void InitializeComponent() this.label6.TabIndex = 47; this.label6.Text = "broker ip address or hostname"; // + // label1 + // + this.label1.AutoSize = true; + this.label1.Font = new System.Drawing.Font("Segoe UI", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label1.Location = new System.Drawing.Point(44, 443); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(134, 13); + this.label1.TabIndex = 67; + this.label1.Text = "(leave empty for random)"; + // + // TbMqttClientId + // + this.TbMqttClientId.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); + this.TbMqttClientId.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.TbMqttClientId.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.TbMqttClientId.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); + this.TbMqttClientId.Location = new System.Drawing.Point(45, 415); + this.TbMqttClientId.MaxLength = 100; + this.TbMqttClientId.Name = "TbMqttClientId"; + this.TbMqttClientId.Size = new System.Drawing.Size(191, 25); + this.TbMqttClientId.TabIndex = 65; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Font = new System.Drawing.Font("Segoe UI", 9.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.label2.Location = new System.Drawing.Point(44, 395); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(53, 17); + this.label2.TabIndex = 66; + this.label2.Text = "client id"; + // // MQTT // this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); + this.Controls.Add(this.label1); + this.Controls.Add(this.TbMqttClientId); + this.Controls.Add(this.label2); this.Controls.Add(this.label27); this.Controls.Add(this.TbMqttClientCertificate); this.Controls.Add(this.label26); @@ -338,7 +377,7 @@ private void InitializeComponent() this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); this.Margin = new System.Windows.Forms.Padding(4); this.Name = "MQTT"; - this.Size = new System.Drawing.Size(700, 457); + this.Size = new System.Drawing.Size(700, 486); this.Load += new System.EventHandler(this.MQTT_Load); ((System.ComponentModel.ISupportInitialize)(this.TbIntMqttPort)).EndInit(); this.ResumeLayout(false); @@ -369,5 +408,8 @@ private void InitializeComponent() internal System.Windows.Forms.TextBox TbMqttUsername; internal System.Windows.Forms.TextBox TbMqttAddress; internal System.Windows.Forms.CheckBox CbMqttTls; + private System.Windows.Forms.Label label1; + internal System.Windows.Forms.TextBox TbMqttClientId; + private System.Windows.Forms.Label label2; } } diff --git a/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.cs b/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.cs index d8c4367..c475b47 100644 --- a/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.cs +++ b/src/HASS.Agent/HASSAgent/Controls/Configuration/MQTT.cs @@ -59,6 +59,7 @@ private void BtnMqttClearConfig_Click(object sender, EventArgs e) TbMqttUsername.Text = string.Empty; TbMqttPassword.Text = string.Empty; TbMqttDiscoveryPrefix.Text = "homeassistant"; + TbMqttClientId.Text = string.Empty; TbMqttRootCertificate.Text = string.Empty; TbMqttClientCertificate.Text = string.Empty; CbAllowUntrustedCertificates.CheckState = CheckState.Checked; diff --git a/src/HASS.Agent/HASSAgent/Forms/Configuration.cs b/src/HASS.Agent/HASSAgent/Forms/Configuration.cs index daaef0d..1cc9a06 100644 --- a/src/HASS.Agent/HASSAgent/Forms/Configuration.cs +++ b/src/HASS.Agent/HASSAgent/Forms/Configuration.cs @@ -250,6 +250,7 @@ private void LoadSettings() _mqtt.TbMqttUsername.Text = Variables.AppSettings.MqttUsername; _mqtt.TbMqttPassword.Text = Variables.AppSettings.MqttPassword; _mqtt.TbMqttDiscoveryPrefix.Text = Variables.AppSettings.MqttDiscoveryPrefix; + _mqtt.TbMqttClientId.Text = Variables.AppSettings.MqttClientId; _mqtt.TbMqttRootCertificate.Text = Variables.AppSettings.MqttRootCertificate; _mqtt.TbMqttClientCertificate.Text = Variables.AppSettings.MqttClientCertificate; _mqtt.CbAllowUntrustedCertificates.CheckState = Variables.AppSettings.MqttAllowUntrustedCertificates ? CheckState.Checked : CheckState.Unchecked; @@ -324,6 +325,7 @@ private void StoreSettings() Variables.AppSettings.MqttUsername = _mqtt.TbMqttUsername.Text; Variables.AppSettings.MqttPassword = _mqtt.TbMqttPassword.Text; Variables.AppSettings.MqttDiscoveryPrefix = _mqtt.TbMqttDiscoveryPrefix.Text; + Variables.AppSettings.MqttClientId = _mqtt.TbMqttClientId.Text; Variables.AppSettings.MqttRootCertificate = _mqtt.TbMqttRootCertificate.Text; Variables.AppSettings.MqttClientCertificate = _mqtt.TbMqttClientCertificate.Text; Variables.AppSettings.MqttAllowUntrustedCertificates = _mqtt.CbAllowUntrustedCertificates.CheckState == CheckState.Checked; diff --git a/src/HASS.Agent/HASSAgent/Functions/HelperFunctions.cs b/src/HASS.Agent/HASSAgent/Functions/HelperFunctions.cs index 1426b8b..4d8007d 100644 --- a/src/HASS.Agent/HASSAgent/Functions/HelperFunctions.cs +++ b/src/HASS.Agent/HASSAgent/Functions/HelperFunctions.cs @@ -6,7 +6,11 @@ using System.Drawing; using System.IO; using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -25,6 +29,7 @@ using HASSAgent.Mqtt; using HASSAgent.Notifications; using HASSAgent.Sensors; +using MQTTnet.Exceptions; using Serilog; using Syncfusion.Windows.Forms; using Task = System.Threading.Tasks.Task; @@ -95,67 +100,6 @@ internal static void SetMsgBoxStyle(Font font) MessageBoxAdv.MessageFont = font; } - /// - /// Initializes Serilog logger, optionally with exception logging through Coderr - /// - internal static void PrepareLogging() - { - if (Variables.ExceptionLogging) - { - PrepareCoderrEnabledLogging(); - return; - } - - // prepare a serilog logger without coderr - Log.Logger = new LoggerConfiguration() - .WriteTo.Async(a => - a.File(Path.Combine(Variables.LogPath, $"[{DateTime.Now:yyyy-MM-dd}] {Variables.ApplicationName}_.log"), - rollingInterval: RollingInterval.Day, - fileSizeLimitBytes: 10000000, - retainedFileCountLimit: 10, - rollOnFileSizeLimit: true, - buffered: true, - flushToDiskInterval: TimeSpan.FromMilliseconds(150))) - .CreateLogger(); - - Log.Information("[LOG] Coderr exception reporting disabled"); - } - - /// - /// Prepare Serilog logger and bind Coderr reporting - /// - private static void PrepareCoderrEnabledLogging() - { - // initialize coderr - Err.Configuration.ThrowExceptions = false; - - var url = new Uri("https://report.coderr.io/"); - Err.Configuration.Credentials(url, - "b8f26633ad354e91ab570f840080816a", - "87163340045849d993879be4407d952b"); - - Err.Configuration.CatchWinFormsExceptions(); - - Err.Configuration.UserInteraction.AskUserForDetails = false; - Err.Configuration.UserInteraction.AskUserForPermission = false; - Err.Configuration.UserInteraction.AskForEmailAddress = false; - - // prepare a serilog logger including coderr - Log.Logger = new LoggerConfiguration() - .WriteTo.Coderr() - .WriteTo.Async(a => - a.File(Path.Combine(Variables.LogPath, $"[{DateTime.Now:yyyy-MM-dd}] {Variables.ApplicationName}_.log"), - rollingInterval: RollingInterval.Day, - fileSizeLimitBytes: 10000000, - retainedFileCountLimit: 10, - rollOnFileSizeLimit: true, - buffered: true, - flushToDiskInterval: TimeSpan.FromMilliseconds(150))) - .CreateLogger(); - - Log.Information("[LOG] Coderr exception reporting enabled"); - } - /// /// Restarts HASS.Agent through a temporary bat file /// diff --git a/src/HASS.Agent/HASSAgent/Functions/LoggingManager.cs b/src/HASS.Agent/HASSAgent/Functions/LoggingManager.cs new file mode 100644 index 0000000..55cbd76 --- /dev/null +++ b/src/HASS.Agent/HASSAgent/Functions/LoggingManager.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Sockets; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Threading.Tasks; +using Coderr.Client; +using Coderr.Client.Serilog; +using MQTTnet.Exceptions; +using Serilog; + +namespace HASSAgent.Functions +{ + internal static class LoggingManager + { + private static readonly List LoggedFirstChanceHttpRequestExceptions = new List(); + private static string _lastLog = string.Empty; + + /// + /// Initializes Serilog logger, optionally with exception logging through Coderr + /// + internal static void PrepareLogging() + { + if (Variables.ExceptionLogging) + { + PrepareCoderrEnabledLogging(); + return; + } + + // prepare a serilog logger without coderr + Log.Logger = new LoggerConfiguration() + .WriteTo.Async(a => + a.File(Path.Combine(Variables.LogPath, $"[{DateTime.Now:yyyy-MM-dd}] {Variables.ApplicationName}_.log"), + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 10000000, + retainedFileCountLimit: 10, + rollOnFileSizeLimit: true, + buffered: true, + flushToDiskInterval: TimeSpan.FromMilliseconds(150))) + .CreateLogger(); + + Log.Information("[LOG] Coderr exception reporting disabled"); + } + + /// + /// Prepare Serilog logger and bind Coderr reporting + /// + private static void PrepareCoderrEnabledLogging() + { + // initialize coderr + Err.Configuration.ThrowExceptions = false; + + var url = new Uri("https://report.coderr.io/"); + Err.Configuration.Credentials(url, + "b8f26633ad354e91ab570f840080816a", + "87163340045849d993879be4407d952b"); + + Err.Configuration.CatchWinFormsExceptions(); + + Err.Configuration.UserInteraction.AskUserForDetails = false; + Err.Configuration.UserInteraction.AskUserForPermission = false; + Err.Configuration.UserInteraction.AskForEmailAddress = false; + + // prepare a serilog logger including coderr + Log.Logger = new LoggerConfiguration() + .WriteTo.Coderr() + .WriteTo.Async(a => + a.File(Path.Combine(Variables.LogPath, $"[{DateTime.Now:yyyy-MM-dd}] {Variables.ApplicationName}_.log"), + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 10000000, + retainedFileCountLimit: 10, + rollOnFileSizeLimit: true, + buffered: true, + flushToDiskInterval: TimeSpan.FromMilliseconds(150))) + .CreateLogger(); + + Log.Information("[LOG] Coderr exception reporting enabled"); + } + + /// + /// Processes FirstChanceExceptions (when extended logging's enabled) + /// + /// + /// + /// + internal static void CurrentDomainOnFirstChanceException(object sender, FirstChanceExceptionEventArgs eventArgs) + { + try + { + // resource exceptions can be ignored + if (eventArgs.Exception.Message.Contains("GetLocalizedResourceManager")) return; + + // syncfusion input-string errors as well + if (eventArgs.Exception.Message.Contains("IntegerTextBox.FormatChanged")) return; + + // we only log these once-in-a-row, to prevent spamming + if (!string.IsNullOrEmpty(_lastLog)) + { + if (_lastLog == eventArgs.Exception.ToString()) return; + if (eventArgs.Exception.ToString().Contains(_lastLog)) return; + } + _lastLog = eventArgs.Exception.ToString(); + + // handle based on exception type + switch (eventArgs.Exception) + { + // handle filenotfound exceptions + case FileNotFoundException fileNotFoundException: + HandleFirstChanceFileNotFoundException(fileNotFoundException); + return; + + // handle socket exceptions + case SocketException socketException: + HandleFirstChanceSocketException(socketException); + return; + + // handle web exceptions + case WebException webException: + HandleFirstChanceWebException(webException); + return; + + // handle httprequest exceptions + case HttpRequestException httpRequestException: + HandleFirstChanceHttpRequestException(httpRequestException); + return; + + // handle mqttcommunication exceptions + case MqttCommunicationException mqttCommunicationException: + HandleFirstChanceMqttCommunicationException(mqttCommunicationException); + return; + + // just log it + default: + Log.Fatal(eventArgs.Exception, "[PROGRAM] FirstChanceException: {err}", eventArgs.Exception.Message); + break; + } + } + catch (Exception ex) + { + Log.Fatal(ex, "[PROGRAM] Error processing FirstChanceException: {err}", ex.Message); + } + } + + private static void HandleFirstChanceFileNotFoundException(FileNotFoundException fileNotFoundException) + { + // ignore resources + if (fileNotFoundException.FileName.Contains("resources")) return; + + Log.Error("[PROGRAM] FileNotFoundException: {err}", fileNotFoundException.Message); + } + + private static void HandleFirstChanceSocketException(SocketException socketException) + { + var socketErrorCode = socketException.SocketErrorCode; + switch (socketErrorCode) + { + case SocketError.ConnectionRefused: + Log.Error("[NET] [{type}] {err}", socketErrorCode.ToString(), socketException.Message); + return; + + case SocketError.ConnectionReset: + Log.Error("[NET] [{type}] {err}", socketErrorCode.ToString(), socketException.Message); + return; + + default: + Log.Fatal(socketException, "[NET] [{type}] {err}", socketErrorCode.ToString(), socketException.Message); + break; + } + } + + private static void HandleFirstChanceWebException(WebException webException) + { + var status = webException.Status; + switch (status) + { + case WebExceptionStatus.ConnectFailure: + Log.Error("[NET] [{type}] {err}", status.ToString(), webException.Message); + return; + + case WebExceptionStatus.Timeout: + Log.Error("[NET] [{type}] {err}", status.ToString(), webException.Message); + return; + + default: + Log.Fatal(webException, "[NET] [{type}] {err}", status.ToString(), webException.Message); + return; + } + } + + private static void HandleFirstChanceHttpRequestException(HttpRequestException httpRequestException) + { + // usually contains a more interesting inner exception + if (httpRequestException.InnerException != null) + { + switch (httpRequestException.InnerException) + { + case SocketException sE: + HandleFirstChanceSocketException(sE); + break; + case WebException wE: + HandleFirstChanceWebException(wE); + break; + } + } + + // only log once to prevent spamming + var excMsg = httpRequestException.ToString(); + if (LoggedFirstChanceHttpRequestExceptions.Contains(excMsg)) return; + LoggedFirstChanceHttpRequestExceptions.Add(excMsg); + + if (excMsg.Contains("SocketException")) + { + Log.Error("[NET] [{type}] {err}", "FirstChanceHttpRequestException.SocketException", httpRequestException.Message); + return; + } + + // just log it + Log.Fatal(httpRequestException, "[NET] FirstChanceHttpRequestException: {err}", httpRequestException.Message); + } + + private static void HandleFirstChanceMqttCommunicationException(MqttCommunicationException mqttCommunicationException) + { + // usually contains a more interesting inner exception + if (mqttCommunicationException.InnerException != null) + { + switch (mqttCommunicationException.InnerException) + { + case SocketException sE: + HandleFirstChanceSocketException(sE); + break; + case WebException wE: + HandleFirstChanceWebException(wE); + break; + } + } + + // check for exceptions in message + var excMsg = mqttCommunicationException.ToString(); + if (excMsg.Contains("SocketException")) + { + Log.Error("[NET] [{type}] {err}", "MqttCommunicationException.SocketException", mqttCommunicationException.Message); + return; + } + if (excMsg.Contains("OperationCanceledException")) + { + Log.Error("[NET] [{type}] {err}", "MqttCommunicationException.OperationCanceledException", mqttCommunicationException.Message); + return; + } + + // just log it + Log.Fatal(mqttCommunicationException, "[NET] FirstChancemqttCommunicationException: {err}", mqttCommunicationException.Message); + } + } +} diff --git a/src/HASS.Agent/HASSAgent/HASSAgent.csproj b/src/HASS.Agent/HASSAgent/HASSAgent.csproj index 8515591..248cb0b 100644 --- a/src/HASS.Agent/HASSAgent/HASSAgent.csproj +++ b/src/HASS.Agent/HASSAgent/HASSAgent.csproj @@ -280,6 +280,7 @@ + diff --git a/src/HASS.Agent/HASSAgent/HomeAssistant/HassApiManager.cs b/src/HASS.Agent/HASSAgent/HomeAssistant/HassApiManager.cs index b86c6ff..6699b5a 100644 --- a/src/HASS.Agent/HASSAgent/HomeAssistant/HassApiManager.cs +++ b/src/HASS.Agent/HASSAgent/HomeAssistant/HassApiManager.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Net.Http; using System.Security.Cryptography.X509Certificates; +using System.Threading; using System.Threading.Tasks; using HADotNet.Core; using HADotNet.Core.Clients; @@ -22,9 +23,9 @@ namespace HASSAgent.HomeAssistant /// internal static class HassApiManager { - private static ConfigClient _configClient; - private static ServiceClient _serviceClient; - private static EntityClient _entityClient; + private static ConfigClient _configClient = null; + private static ServiceClient _serviceClient = null; + private static EntityClient _entityClient = null; private static StatesClient _statesClient = null; internal static HassManagerStatus ManagerStatus = HassManagerStatus.Initialising; @@ -43,6 +44,10 @@ internal static class HassApiManager private static readonly string[] OnStates = { "on", "playing", "open", "opening" }; private static readonly string[] OffStates = { "off", "idle", "paused", "stopped", "closed", "closing" }; + private static readonly List NotFoundEntities = new List(); + + private static readonly SemaphoreSlim ConfigCheckSemaphore = new SemaphoreSlim(1, 1); + /// /// Initializes the HASS API manager, establishes a connection and loads the entities /// @@ -174,11 +179,21 @@ private static async Task GetConfig() var runningTimer = Stopwatch.StartNew(); Exception err = null; - // prepare a config client - _configClient = ClientFactory.GetClient(); + // make sure clientfactory's initialized + if (!ClientFactory.IsInitialized) + { + InitializeClient(); + + if (_serviceClient == null) _serviceClient = ClientFactory.GetClient(); + if (_serviceClient == null) _entityClient = ClientFactory.GetClient(); + if (_statesClient == null) _statesClient = ClientFactory.GetClient(); + } + + // create config client + if (_configClient == null) _configClient = ClientFactory.GetClient(); // start trying during the grace period - while (runningTimer.Elapsed.Seconds < Variables.AppSettings.DisconnectedGracePeriodSeconds) + while (runningTimer.Elapsed.TotalSeconds < Variables.AppSettings.DisconnectedGracePeriodSeconds) { try { @@ -186,6 +201,7 @@ private static async Task GetConfig() var config = await _configClient.GetConfiguration(); // if we're here, the connection works + // only log if the version changed if (config.Version == _haVersion) return true; // version changed since last check (or this is the first check), log @@ -210,7 +226,14 @@ private static async Task GetConfig() } } - // if we're here, set failed state and log + // if we're here, reset clients, set failed state and log + ClientFactory.Reset(); + + _configClient = null; + _serviceClient = null; + _entityClient = null; + _statesClient = null; + Variables.MainForm?.SetHassApiStatus(ComponentStatus.Failed); ManagerStatus = HassManagerStatus.Failed; @@ -223,26 +246,40 @@ private static async Task GetConfig() /// Checks if the connection's working, if not, will retry for max 60 seconds through GetConfig() /// /// - private static async Task CheckConnection() + private static async Task CheckConnectionAsync() { - // check if we can connect - if (!await GetConfig()) return false; + await ConfigCheckSemaphore.WaitAsync(); - // optionally reset failed state - if (ManagerStatus == HassManagerStatus.Failed) + try { - // reset failed state and log - ManagerStatus = HassManagerStatus.Ready; - Variables.MainForm?.SetHassApiStatus(ComponentStatus.Ok); + // check if we can connect + if (!await GetConfig()) return false; - Log.Information("[HASS_API] Server recovered from failed state"); + // optionally reset failed state + if (ManagerStatus != HassManagerStatus.Ready) + { + // set new clients + _serviceClient = ClientFactory.GetClient(); + _entityClient = ClientFactory.GetClient(); + _statesClient = ClientFactory.GetClient(); - // reset all sensors so they'll republish - SensorsManager.ResetAllSensorChecks(); - } + // reset failed state and log + ManagerStatus = HassManagerStatus.Ready; + Variables.MainForm?.SetHassApiStatus(ComponentStatus.Ok); + + Log.Information("[HASS_API] Server recovered from failed state"); - // all good - return true; + // reset all sensors so they'll republish + SensorsManager.ResetAllSensorChecks(); + } + + // all good + return true; + } + finally + { + ConfigCheckSemaphore.Release(); + } } /// @@ -520,7 +557,7 @@ private static async void PeriodicEntityReload() await Task.Delay(TimeSpan.FromMinutes(5)); // check if the connection's still up - if (!await CheckConnection()) return; + if (!await CheckConnectionAsync()) continue; // reload all entities await LoadEntitiesAsync(true); @@ -538,7 +575,7 @@ private static async void PeriodicStatusUpdates() await Task.Delay(TimeSpan.FromSeconds(3)); // check if the connection's still up - if (!await CheckConnection()) continue; + if (!await CheckConnectionAsync()) continue; foreach (var quickAction in Variables.QuickActions) { @@ -567,8 +604,14 @@ private static async void PeriodicStatusUpdates() if (ex.Message.Contains("404")) { - Log.Warning("[HASS_API] Server returned 404 (not found) while getting entity state. This can happen after a server reboot, or if you've deleted the entity. If the problem persists, please file a ticket on github.\r\nEntity: {entity}\r\nError message: {err}", $"{quickAction.Domain}.{quickAction.Entity}", ex.Message); - return; + var notFoundEntity = $"{quickAction.Domain.ToString().ToLower()}.{quickAction.Entity.ToLower()}"; + + // log only once + if (NotFoundEntities.Contains(notFoundEntity)) continue; + NotFoundEntities.Add(notFoundEntity); + + Log.Warning("[HASS_API] Server returned 404 (not found) while getting entity state. This can happen after a server reboot, or if you've deleted the entity. If the problem persists, please file a ticket on github.\r\nEntity: {entity}\r\nError message: {err}", notFoundEntity, ex.Message); + continue; } // only log errors once to prevent log spamming diff --git a/src/HASS.Agent/HASSAgent/Models/Config/AppSettings.cs b/src/HASS.Agent/HASSAgent/Models/Config/AppSettings.cs index 07061dd..ce4b0c0 100644 --- a/src/HASS.Agent/HASSAgent/Models/Config/AppSettings.cs +++ b/src/HASS.Agent/HASSAgent/Models/Config/AppSettings.cs @@ -49,6 +49,7 @@ public AppSettings() public string MqttUsername { get; set; } public string MqttPassword { get; set; } public string MqttDiscoveryPrefix { get; set; } = "homeassistant"; + public string MqttClientId { get; set; } = string.Empty; public bool MqttUseRetainFlag { get; set; } = true; public string MqttRootCertificate { get; set; } public string MqttClientCertificate { get; set; } diff --git a/src/HASS.Agent/HASSAgent/Mqtt/MqttManager.cs b/src/HASS.Agent/HASSAgent/Mqtt/MqttManager.cs index 3afecce..872932a 100644 --- a/src/HASS.Agent/HASSAgent/Mqtt/MqttManager.cs +++ b/src/HASS.Agent/HASSAgent/Mqtt/MqttManager.cs @@ -14,6 +14,7 @@ using HASSAgent.Models.HomeAssistant; using HASSAgent.Models.HomeAssistant.Commands; using HASSAgent.Sensors; +using HASSAgent.Settings; using MQTTnet; using MQTTnet.Adapter; using MQTTnet.Client; @@ -146,8 +147,12 @@ private static async void ConnectingFailedHandler(ManagedProcessFailedEventArgs { if (_mqttClient.IsConnected) { - // recoved, nevermind + // recovered, nevermind + if (_status == MqttStatus.Connected) return; + _status = MqttStatus.Connected; Variables.MainForm?.SetMqttStatus(ComponentStatus.Ok); + + Log.Information("[MQTT] Connected"); return; } @@ -162,7 +167,10 @@ private static async void ConnectingFailedHandler(ManagedProcessFailedEventArgs if (_connectingFailureNotified) return; _connectingFailureNotified = true; - Log.Fatal(ex.Exception, "[MQTT] Error while connecting: {err}", ex.Exception.Message); + var excMsg = ex.Exception.ToString(); + if (excMsg.Contains("SocketException")) Log.Error("[MQTT] Error while connecting: {err}", ex.Exception.Message); + else if (excMsg.Contains("MqttCommunicationTimedOutException")) Log.Error("[MQTT] Error while connecting: {err}", "Connection timed out"); + else Log.Fatal(ex.Exception, "[MQTT] Error while connecting: {err}", ex.Exception.Message); Variables.MainForm?.ShowToolTip("mqtt: failed to connect", true); } @@ -182,8 +190,12 @@ private static async void DisconnectedHandler(MqttClientDisconnectedEventArgs e) { if (_mqttClient.IsConnected) { - // recoved, nevermind + // recovered, nevermind + if (_status == MqttStatus.Connected) return; + _status = MqttStatus.Connected; Variables.MainForm?.SetMqttStatus(ComponentStatus.Ok); + + Log.Information("[MQTT] Connected"); return; } @@ -513,9 +525,13 @@ internal static async Task UnubscribeAsync(AbstractCommand command) private static ManagedMqttClientOptions GetOptions() { if (string.IsNullOrEmpty(Variables.AppSettings.MqttAddress)) return null; - - // id can be random - var clientId = Guid.NewGuid().ToString().Substring(0, 8); + + // id can be random, but we'll store it for consistency (unless user-defined) + if (string.IsNullOrEmpty(Variables.AppSettings.MqttClientId)) + { + Variables.AppSettings.MqttClientId = Guid.NewGuid().ToString().Substring(0, 8); + SettingsManager.StoreAppSettings(); + } // configure last will message // todo: cover other domains @@ -531,7 +547,7 @@ private static ManagedMqttClientOptions GetOptions() // basic options var clientOptionsBuilder = new MqttClientOptionsBuilder() - .WithClientId(clientId) + .WithClientId(Variables.AppSettings.MqttClientId) .WithTcpServer(Variables.AppSettings.MqttAddress, Variables.AppSettings.MqttPort) .WithCleanSession() .WithWillMessage(lastWillMessage) diff --git a/src/HASS.Agent/HASSAgent/Program.cs b/src/HASS.Agent/HASSAgent/Program.cs index 50e208b..eb69bc6 100644 --- a/src/HASS.Agent/HASSAgent/Program.cs +++ b/src/HASS.Agent/HASSAgent/Program.cs @@ -33,22 +33,12 @@ private static void Main(string[] args) Variables.ExceptionLogging = SettingsManager.GetExceptionReportingSetting(); // enable logging and optionally prepare Coderr - HelperFunctions.PrepareLogging(); + LoggingManager.PrepareLogging(); if (Variables.ExtendedLogging) { // make sure we catch 'm all - AppDomain.CurrentDomain.FirstChanceException += (sender, eventArgs) => - { - // resource exceptions can be ignored - if (eventArgs.Exception.Message.Contains("GetLocalizedResourceManager")) return; - - // syncfusion input-string errors as well - if (eventArgs.Exception.Message.Contains("IntegerTextBox.FormatChanged")) return; - - // log it - Log.Fatal(eventArgs.Exception, "[PROGRAM] FirstChanceException: {err}", eventArgs.Exception.Message); - }; + AppDomain.CurrentDomain.FirstChanceException += LoggingManager.CurrentDomainOnFirstChanceException; } else Log.Information("[PROGRAM] Extended logging disabled"); diff --git a/src/HASS.Agent/HASSAgent/Properties/AssemblyInfo.cs b/src/HASS.Agent/HASSAgent/Properties/AssemblyInfo.cs index fad7835..1abb00c 100644 --- a/src/HASS.Agent/HASSAgent/Properties/AssemblyInfo.cs +++ b/src/HASS.Agent/HASSAgent/Properties/AssemblyInfo.cs @@ -31,5 +31,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("2022.2.27.0")] -[assembly: AssemblyFileVersion("2022.2.27.0")] +[assembly: AssemblyVersion("2022.3.1.0")] +[assembly: AssemblyFileVersion("2022.3.1.0")]