Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions NetDaemon.Extensions.MqttEntities/DependencyInjectionSetup.cs
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)
Copy link
Collaborator

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

{
services.AddSingleton<IMqttFactory, MqttFactory>();
services.AddSingleton<IEntityUpdater, EntityUpdater>();
services.AddSingleton<IMessageSender, MessageSender>();
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
31 changes: 31 additions & 0 deletions NetDaemon.Extensions.MqttEntities/EntityUpdater.cs
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a ConfigureAawait(false). Even if chances are slim this is used by a UI app we did this in the rest of assemblies

}
}
6 changes: 6 additions & 0 deletions NetDaemon.Extensions.MqttEntities/IEntityUpdater.cs
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);
}
6 changes: 6 additions & 0 deletions NetDaemon.Extensions.MqttEntities/IMessageSender.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace NetDaemon.Extensions.MqttEntities;

public interface IMessageSender
Copy link
Collaborator

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - definitely should be internal!

{
Task SendMessageAsync(string topic, string payload);
}
64 changes: 64 additions & 0 deletions NetDaemon.Extensions.MqttEntities/MessageSender.cs
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")
Copy link
Contributor

Choose a reason for hiding this comment

The 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())
Copy link
Contributor

Choose a reason for hiding this comment

The 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);
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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
}
}
9 changes: 9 additions & 0 deletions NetDaemon.Extensions.MqttEntities/MqttConfiguration.cs
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; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these properties really nullable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably not - I'll update - thanks :)

Copy link
Contributor

Choose a reason for hiding this comment

The 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>
15 changes: 15 additions & 0 deletions NetDaemon.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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}
Expand Down