diff --git a/NetDaemon.Extensions.MqttEntities/DependencyInjectionSetup.cs b/NetDaemon.Extensions.MqttEntities/DependencyInjectionSetup.cs new file mode 100644 index 000000000..b07329d1b --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/DependencyInjectionSetup.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; +using MQTTnet; + +namespace NetDaemon.Extensions.MqttEntities; + +public static class DependencyInjectionSetup +{ + /// + /// Adds scheduling capabilities through dependency injection + /// + /// Provided service collection + public static IServiceCollection AddMqttExtensions(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + return services; + } +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/EntityUpdater.cs b/NetDaemon.Extensions.MqttEntities/EntityUpdater.cs new file mode 100644 index 000000000..a9c046405 --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/EntityUpdater.cs @@ -0,0 +1,31 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace NetDaemon.Extensions.MqttEntities; + +public class EntityUpdater : IEntityUpdater +{ + private readonly ILogger _logger; + private readonly IMessageSender _messageSender; + + public EntityUpdater(ILogger logger, IMessageSender messageSender) + { + _logger = logger; + _messageSender = messageSender; + } + + public async Task CreateAsync(string deviceType, string deviceClass, string entityId, string name) + { + var rootPath = $"homeassistant/{deviceType}/{entityId}"; + var topicPath = $"{rootPath}/config"; + var statePath = $"{rootPath}/state"; + var attrsPath = $"{rootPath}/attributes"; + + var payload = JsonSerializer.Serialize( new + { + name = name, device_class = deviceClass, state_topic = statePath, json_attributes_topic = attrsPath + }); + + await _messageSender.SendMessageAsync( topicPath, payload); + } +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/IEntityUpdater.cs b/NetDaemon.Extensions.MqttEntities/IEntityUpdater.cs new file mode 100644 index 000000000..0a22f0b65 --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/IEntityUpdater.cs @@ -0,0 +1,6 @@ +namespace NetDaemon.Extensions.MqttEntities; + +public interface IEntityUpdater +{ + Task CreateAsync(string deviceType, string deviceClass, string entityId, string name); +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/IMessageSender.cs b/NetDaemon.Extensions.MqttEntities/IMessageSender.cs new file mode 100644 index 000000000..b2663117f --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/IMessageSender.cs @@ -0,0 +1,6 @@ +namespace NetDaemon.Extensions.MqttEntities; + +public interface IMessageSender +{ + Task SendMessageAsync(string topic, string payload); +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/MessageSender.cs b/NetDaemon.Extensions.MqttEntities/MessageSender.cs new file mode 100644 index 000000000..5642e7583 --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/MessageSender.cs @@ -0,0 +1,64 @@ +using System.Security.Authentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; + +namespace NetDaemon.Extensions.MqttEntities; + +internal class MessageSender : IMessageSender +{ + private readonly ILogger _logger; + private readonly IMqttFactory _mqttFactory; + private readonly MqttConfiguration? _mqttConfig; + + public MessageSender(ILogger logger, IConfiguration configuration, IMqttFactory mqttFactory) + { + _logger = logger; + _mqttFactory = mqttFactory; + + _mqttConfig = configuration.GetSection("Mqtt") + .Get(); + + _logger.LogDebug($"MQTT connection is {_mqttConfig?.Host}:{_mqttConfig?.Port}/{_mqttConfig?.UserId}"); + } + + public async Task SendMessageAsync(string topic, string payload) + { + using (var mqttClient = _mqttFactory.CreateMqttClient()) + { + await ConnectAsync(mqttClient); + await PublishMessage(mqttClient, topic, payload); + } + } + + private async Task ConnectAsync(IMqttClient mqttClient) + { + var options = new MqttClientOptionsBuilder() + .WithTcpServer(_mqttConfig?.Host, _mqttConfig?.Port) + .WithCredentials(_mqttConfig?.UserId, _mqttConfig?.Password) + .Build(); + + var connectResult = await mqttClient.ConnectAsync(options, CancellationToken.None); + if (connectResult.ResultCode != MqttClientConnectResultCode.Success) + throw new AuthenticationException(connectResult.ReasonString); + } + + private async Task PublishMessage(IApplicationMessagePublisher client, string topic, string payload) + { + var message = new MqttApplicationMessageBuilder() + .WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag() + .Build(); + + _logger.LogDebug($"Sending to {message.Topic}:\r\n {message.ConvertPayloadToString()}"); + + var publishResult = await client.PublishAsync(message, CancellationToken.None); + if (publishResult.ReasonCode != MqttClientPublishReasonCode.Success) + throw new InvalidOperationException(publishResult.ReasonString); + } +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/MqttConfiguration.cs b/NetDaemon.Extensions.MqttEntities/MqttConfiguration.cs new file mode 100644 index 000000000..6963b1f9f --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/MqttConfiguration.cs @@ -0,0 +1,9 @@ +namespace NetDaemon.Extensions.MqttEntities; + +internal class MqttConfiguration +{ + public string? Host { get; set; } + public int? Port { get; set; } + public string? UserId { get; set; } + public string? Password { get; set; } +} \ No newline at end of file diff --git a/NetDaemon.Extensions.MqttEntities/NetDaemon.Extensions.MqttEntities.csproj b/NetDaemon.Extensions.MqttEntities/NetDaemon.Extensions.MqttEntities.csproj new file mode 100644 index 000000000..7bd20349a --- /dev/null +++ b/NetDaemon.Extensions.MqttEntities/NetDaemon.Extensions.MqttEntities.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + enable + enable + + + + + + + + + + diff --git a/NetDaemon.sln b/NetDaemon.sln index e6d6d3504..28c2bfb64 100644 --- a/NetDaemon.sln +++ b/NetDaemon.sln @@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Extensions.Loggin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Extensions.Tts", "src\Extensions\NetDaemon.Extensions.Tts\NetDaemon.Extensions.Tts.csproj", "{F4B29B77-9B92-4037-A884-288CA5EF0B78}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Extensions.MqttEntities", "NetDaemon.Extensions.MqttEntities\NetDaemon.Extensions.MqttEntities.csproj", "{DE352CE2-977E-4B29-9055-A71F213A55C5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -269,6 +271,18 @@ Global {F4B29B77-9B92-4037-A884-288CA5EF0B78}.Release|x64.Build.0 = Release|Any CPU {F4B29B77-9B92-4037-A884-288CA5EF0B78}.Release|x86.ActiveCfg = Release|Any CPU {F4B29B77-9B92-4037-A884-288CA5EF0B78}.Release|x86.Build.0 = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|x64.Build.0 = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Debug|x86.Build.0 = Debug|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|Any CPU.Build.0 = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|x64.ActiveCfg = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|x64.Build.0 = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|x86.ActiveCfg = Release|Any CPU + {DE352CE2-977E-4B29-9055-A71F213A55C5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -292,6 +306,7 @@ Global {898966EA-F814-4B7B-9A3D-5E78C38174B2} = {E15D4280-7FFC-4F8B-9B8C-CF9AF2BF838C} {00333EBA-DB52-4D56-ADF7-940FB533E530} = {DFF3E7AA-7A50-4A1E-B3F8-EC01531FB83D} {F4B29B77-9B92-4037-A884-288CA5EF0B78} = {DFF3E7AA-7A50-4A1E-B3F8-EC01531FB83D} + {DE352CE2-977E-4B29-9055-A71F213A55C5} = {DFF3E7AA-7A50-4A1E-B3F8-EC01531FB83D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C5FBB7F-654C-4CAC-964F-6D71AF3D62F8}