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")]