diff --git a/Examples/Configuration/config.spec.yaml b/Examples/Configuration/config.spec.yaml index 9827112..cf93291 100644 --- a/Examples/Configuration/config.spec.yaml +++ b/Examples/Configuration/config.spec.yaml @@ -144,9 +144,26 @@ Overrides: Enabled: false HomeAssistant: + # Is home assistant mQTT-discovery enabled? + # If you want to manually create entities, or, just want your PDU data pushed to MQTT without home-assistant, change to false. + # (Optional) Default: false DiscoveryEnabled: true + + # Should discovery messages be retained? Strongly recommend leaving this to true! + # (Optional) Default: true + DiscoveryRetain: true + + # This is the home-assistant discovery topic. + # Should prob leave this set as-is. DiscoveryTopic: "homeassistant/discovery" + + # How often should discovery messages be sent? + # 0 = Send Once at startup. + # Note, if you have DiscoveryRetain=true, the only benefit to running more discovery jobs- is to update the names of entityes + # based on labels set in the PDU. + # (Optional) Default: 0 DiscoveryInterval: 300 + # Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable. SensorExpireAfterSeconds: 300 diff --git a/rPDU2MQTT/Classes/MqttEventHandler.cs b/rPDU2MQTT/Classes/MqttEventHandler.cs new file mode 100644 index 0000000..5617b52 --- /dev/null +++ b/rPDU2MQTT/Classes/MqttEventHandler.cs @@ -0,0 +1,53 @@ +using HiveMQtt.Client; +using System.Diagnostics; +using System.Timers; +using Timer = System.Timers.Timer; + +namespace rPDU2MQTT.Classes; + +public class MqttEventHandler +{ + private readonly HiveMQClient client; + private readonly Timer _timer; + public MqttEventHandler(HiveMQClient client) + { + this.client = client; + client.AfterConnect += Client_AfterConnect; + client.OnDisconnectSent += Client_OnDisconnectSent; + client.OnDisconnectReceived += Client_OnDisconnectReceived; + + _timer = new Timer(TimeSpan.FromSeconds(10)); + _timer.Elapsed += HealthTimer; + _timer.AutoReset = true; // Ensures the event fires repeatedly at the interval + _timer.Enabled = true; // Starts the timer + } + + private void Client_OnDisconnectReceived(object? sender, HiveMQtt.Client.Events.OnDisconnectReceivedEventArgs e) + { + Log.Error("Received disconnect from broker."); + } + + private void Client_OnDisconnectSent(object? sender, HiveMQtt.Client.Events.OnDisconnectSentEventArgs e) + { + Log.Information("Sending disconnect to broker"); + } + + private void HealthTimer(object? sender, ElapsedEventArgs e) + { + Log.Verbose("Timer.Tick()"); + sendStatusOnline(); + + } + + private void Client_AfterConnect(object? sender, HiveMQtt.Client.Events.AfterConnectEventArgs e) + { + Log.Information("MQTT Client Connected"); + sendStatusOnline(); + } + + + private void sendStatusOnline() + { + TaskManager.AddTask(() => client.PublishAsync(client.Options!.LastWillAndTestament!.Topic, "online", HiveMQtt.MQTT5.Types.QualityOfService.AtLeastOnceDelivery)); + } +} diff --git a/rPDU2MQTT/Classes/TaskManager.cs b/rPDU2MQTT/Classes/TaskManager.cs new file mode 100644 index 0000000..a1730e4 --- /dev/null +++ b/rPDU2MQTT/Classes/TaskManager.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; + +namespace rPDU2MQTT.Classes; + +/// +/// This class, exists to watch tasks until completion. Non-async code, which needs to execute a task, just passes the task off to this class. +/// +public static class TaskManager +{ + private static readonly ConcurrentBag taskBag = new ConcurrentBag(); + + public static void AddTask(Action task) + { + Task newTask = Task.Run(task); + taskBag.Add(newTask); + } + + public static void AddTask(Task task) + { + taskBag.Add(task); + } + + public static void WaitForAllTasks() + { + foreach (var task in taskBag) + { + task.Wait(); + } + + taskBag.Clear(); // ConcurrentBag doesn't have a clear method, but assigning a new one is equivalent to clearing. + } +} diff --git a/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs b/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs index 639b475..b2c2c02 100644 --- a/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs +++ b/rPDU2MQTT/Models/Config/HomeAssistantConfig.cs @@ -20,7 +20,15 @@ public class HomeAssistantConfig /// /// How often should discovery data be published? /// - public int DiscoveryInterval { get; set; } = 300; + /// + /// A value of 0, will run a single discovery, and not run a re-discovery until the application is restarted. + /// + public int DiscoveryInterval { get; set; } = 0; + + /// + /// Should discovery messages be retained? + /// + public bool DiscoveryRetain { get; set; } = true; /// /// Default expireAfter interval applied to all sensors. After this time- the sensor will be marked as unavailable. diff --git a/rPDU2MQTT/Program.cs b/rPDU2MQTT/Program.cs index 58d9eb4..e479295 100644 --- a/rPDU2MQTT/Program.cs +++ b/rPDU2MQTT/Program.cs @@ -23,7 +23,6 @@ //Ensure we can actually connect to MQTT. var client = host.Services.GetRequiredService(); -var logger = host.Services.GetRequiredService>(); Log.Information($"Connecting to MQTT Broker at {client.Options.Host}:{client.Options.Port}"); diff --git a/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs b/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs index 4e75881..1182f83 100644 --- a/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs +++ b/rPDU2MQTT/Services/baseTypes/baseDiscoveryService.cs @@ -110,7 +110,8 @@ protected Task PushDiscoveryMessage(T sensor, CancellationToken cancellationT var msg = new MQTT5PublishMessage(topic, QualityOfService.AtLeastOnceDelivery) { ContentType = "json", - PayloadAsString = System.Text.Json.JsonSerializer.Serialize(sensor, this.jsonOptions) + PayloadAsString = System.Text.Json.JsonSerializer.Serialize(sensor, this.jsonOptions), + Retain = cfg.HASS.DiscoveryRetain, }; if (cfg.Debug.PrintDiscovery) diff --git a/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs b/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs index da420ab..c8e39d5 100644 --- a/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs +++ b/rPDU2MQTT/Services/baseTypes/baseMQTTTService.cs @@ -12,9 +12,8 @@ namespace rPDU2MQTT.Services.baseTypes; /// public abstract class baseMQTTTService : IHostedService, IDisposable { - private readonly int interval; private IHiveMQClient mqtt { get; init; } - private PeriodicTimer timer; + private PeriodicTimer? timer; private Task timerTask = Task.CompletedTask; protected Config cfg { get; } protected PDU pdu { get; } @@ -24,11 +23,16 @@ public abstract class baseMQTTTService : IHostedService, IDisposable protected baseMQTTTService(MQTTServiceDependancies dependancies) : this(dependancies, dependancies.Cfg.PDU.PollInterval) { } protected baseMQTTTService(MQTTServiceDependancies dependancies, int Interval) { - interval = Interval; mqtt = dependancies.Mqtt; cfg = dependancies.Cfg; pdu = dependancies.PDU; - timer = new PeriodicTimer(TimeSpan.FromSeconds(Interval)); + + // If the interval is 0, don't create a timer. + if (Interval <= 0) + timer = null; + else + timer = new PeriodicTimer(TimeSpan.FromSeconds(Interval)); + jsonOptions = new System.Text.Json.JsonSerializerOptions { WriteIndented = true, @@ -46,6 +50,14 @@ protected baseMQTTTService(MQTTServiceDependancies dependancies, int Interval) public async Task StartAsync(CancellationToken cancellationToken) { + if (timer is null) + { + Log.Information($"{GetType().Name} - performing single discovery."); + await Execute(cancellationToken); + return; + + } + Log.Information($"{GetType().Name} is starting."); timerTask = Task.Run(() => timerTaskExecution(cancellationToken).Wait()); @@ -53,6 +65,7 @@ public async Task StartAsync(CancellationToken cancellationToken) //Kick off the first one manually. await Execute(cancellationToken); + // Log message to indicate the service has been started. Log.Information($"{GetType().Name} is running."); } @@ -75,9 +88,14 @@ public Task StopAsync(CancellationToken stoppingToken) return Task.CompletedTask; } + ~baseMQTTTService() + { + Log.Debug($"{GetType().Name} has been finalized"); + } + public void Dispose() { - timer.Dispose(); + timer?.Dispose(); } /// diff --git a/rPDU2MQTT/Startup/ServiceConfiguration.cs b/rPDU2MQTT/Startup/ServiceConfiguration.cs index 0646053..592c979 100644 --- a/rPDU2MQTT/Startup/ServiceConfiguration.cs +++ b/rPDU2MQTT/Startup/ServiceConfiguration.cs @@ -30,11 +30,16 @@ public static void Configure(HostBuilderContext context, IServiceCollection serv ThrowError.TestRequiredConfigurationSection(cfg.MQTT.Connection.Host, "MQTT.Connection.Host"); ThrowError.TestRequiredConfigurationSection(cfg.MQTT.Connection.Host, "MQTT.Connection.Port"); + var lwt = new LastWillAndTestament( + MQTTHelper.JoinPaths(cfg.MQTT.ParentTopic, "Status"), payload: "offline", HiveMQtt.MQTT5.Types.QualityOfService.AtLeastOnceDelivery, true); + var mqttBuilder = new HiveMQClientOptionsBuilder() .WithBroker(cfg.MQTT.Connection.Host) .WithPort(cfg.MQTT.Connection.Port!.Value) - .WithClientId(cfg.MQTT.ClientID ?? "rpdu2mqtt") - .WithAutomaticReconnect(true); + .WithClientId((cfg.MQTT.ClientID ?? "rpdu2mqtt") + Guid.NewGuid().ToString()) + .WithAutomaticReconnect(true) + .WithKeepAlive(cfg.MQTT.KeepAlive) + .WithLastWillAndTestament(lwt); if (cfg.MQTT.Credentials?.Username is not null) mqttBuilder.WithUserName(cfg.MQTT.Credentials.Username); @@ -43,7 +48,13 @@ public static void Configure(HostBuilderContext context, IServiceCollection serv mqttBuilder.WithPassword(cfg.MQTT.Credentials.Password); // Return new client, with options applied. - return new HiveMQClient(mqttBuilder.Build()); + var x = new HiveMQClient(mqttBuilder.Build()); + + // While we are here- lets go ahead and create / bind the event handler. + services.AddSingleton(new MqttEventHandler(x)); + return x; + + }); //Configure Services