-
-
Notifications
You must be signed in to change notification settings - Fork 80
Initial commit of MQTT entity management framework. #656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using MQTTnet; | ||
|
|
||
| namespace NetDaemon.Extensions.MqttEntities; | ||
|
|
||
| public static class DependencyInjectionSetup | ||
| { | ||
| /// <summary> | ||
| /// Adds scheduling capabilities through dependency injection | ||
| /// </summary> | ||
| /// <param name="services">Provided service collection</param> | ||
| public static IServiceCollection AddMqttExtensions(this IServiceCollection services) | ||
| { | ||
| services.AddSingleton<IMqttFactory, MqttFactory>(); | ||
FrankBakkerNl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| services.AddSingleton<IEntityUpdater, EntityUpdater>(); | ||
| services.AddSingleton<IMessageSender, MessageSender>(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The MessageSender does not have any state so it does not have to be a singleton. I read its recimended to use transient in those cases (creating a new instance is relatively cheap) |
||
|
|
||
| return services; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| using System.Text.Json; | ||
| using Microsoft.Extensions.Logging; | ||
|
|
||
| namespace NetDaemon.Extensions.MqttEntities; | ||
|
|
||
| public class EntityUpdater : IEntityUpdater | ||
| { | ||
| private readonly ILogger<EntityUpdater> _logger; | ||
| private readonly IMessageSender _messageSender; | ||
|
|
||
| public EntityUpdater(ILogger<EntityUpdater> 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace NetDaemon.Extensions.MqttEntities; | ||
|
|
||
| public interface IEntityUpdater | ||
| { | ||
| Task CreateAsync(string deviceType, string deviceClass, string entityId, string name); | ||
BeeHiveJava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| namespace NetDaemon.Extensions.MqttEntities; | ||
|
|
||
| public interface IMessageSender | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this intended to be exposed to users or internal interface? If internal make it internal. If it is inteneded to exposed to users. I would think we can have one public API/interface being injected
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes - definitely should be internal! |
||
| { | ||
| Task SendMessageAsync(string topic, string payload); | ||
BeeHiveJava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MessageSender> _logger; | ||
| private readonly IMqttFactory _mqttFactory; | ||
| private readonly MqttConfiguration? _mqttConfig; | ||
|
|
||
| public MessageSender(ILogger<MessageSender> logger, IConfiguration configuration, IMqttFactory mqttFactory) | ||
| { | ||
| _logger = logger; | ||
| _mqttFactory = mqttFactory; | ||
|
|
||
| _mqttConfig = configuration.GetSection("Mqtt") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should use the options framework instead here. |
||
| .Get<MqttConfiguration>(); | ||
|
|
||
| _logger.LogDebug($"MQTT connection is {_mqttConfig?.Host}:{_mqttConfig?.Port}/{_mqttConfig?.UserId}"); | ||
| } | ||
|
|
||
| public async Task SendMessageAsync(string topic, string payload) | ||
| { | ||
| using (var mqttClient = _mqttFactory.CreateMqttClient()) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe cache this client in a field so it is not recreated each time and then dispose in Dispose if this class. |
||
| { | ||
| 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); | ||
BeeHiveJava marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ConfigureAwait (same reason as before, consistency) |
||
| if (publishResult.ReasonCode != MqttClientPublishReasonCode.Success) | ||
| throw new InvalidOperationException(publishResult.ReasonString); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| namespace NetDaemon.Extensions.MqttEntities; | ||
|
|
||
| internal class MqttConfiguration | ||
| { | ||
| public string? Host { get; set; } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these properties really nullable?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably not - I'll update - thanks :)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can probably use a record here with a primary constructor and have the properties auto generated. That way you can make then non nullabe without getting warnings. Username and password might actually be optional |
||
| public int? Port { get; set; } | ||
| public string? UserId { get; set; } | ||
| public string? Password { get; set; } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net6.0</TargetFramework> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <Nullable>enable</Nullable> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.0" /> | ||
| <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> | ||
| <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> | ||
| <PackageReference Include="MQTTnet" Version="3.1.2" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AddNetDaemonMqttEntityManagement or something like that. In the other extensions we hade NetDaemon name here