diff --git a/.github/workflows/push_nuget_manual.yml b/.github/workflows/push_nuget_manual.yml index 22f760165..74c10a2ee 100644 --- a/.github/workflows/push_nuget_manual.yml +++ b/.github/workflows/push_nuget_manual.yml @@ -48,5 +48,7 @@ jobs: run: dotnet pack src/Extensions/NetDaemon.Extensions.Logging/NetDaemon.Extensions.Logging.csproj --configuration Release -p:PackageVersion=${{ github.event.inputs.tag }} - name: 🎁 Pack TTS extensions run: dotnet pack src/Extensions/NetDaemon.Extensions.Tts/NetDaemon.Extensions.Tts.csproj --configuration Release -p:PackageVersion=${{ github.event.inputs.tag }} + - name: 🎁 Pack Mqtt extensions + run: dotnet pack src/Extensions/NetDaemon.Extensions.MqttEntityManager/NetDaemon.Extensions.MqttEntityManager.csproj --configuration Release -p:PackageVersion=${{ github.event.inputs.tag }} - name: 📨 Push to nuget run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --no-symbols true diff --git a/.github/workflows/tags_nuget.yml b/.github/workflows/tags_nuget.yml index 228867c8b..e830a6c48 100644 --- a/.github/workflows/tags_nuget.yml +++ b/.github/workflows/tags_nuget.yml @@ -49,5 +49,7 @@ jobs: run: dotnet pack src/Extensions/NetDaemon.Extensions.Logging/NetDaemon.Extensions.Logging.csproj --configuration Release -p:PackageVersion=${{ steps.version.outputs.version }} - name: 🎁 Pack TTS extensions run: dotnet pack src/Extensions/NetDaemon.Extensions.Tts/NetDaemon.Extensions.Tts.csproj --configuration Release -p:PackageVersion=${{ steps.version.outputs.version }} + - name: 🎁 Pack Mqtt extensions + run: dotnet pack src/Extensions/NetDaemon.Extensions.MqttEntityManager/NetDaemon.Extensions.MqttEntityManager.csproj --configuration Release -p:PackageVersion=${{ steps.version.outputs.version }} - name: 📨 Push to nuget run: dotnet nuget push **/*.nupkg --api-key ${{secrets.NUGET_API_KEY}} --source https://api.nuget.org/v3/index.json --no-symbols true diff --git a/NetDaemon.sln b/NetDaemon.sln index e6d6d3504..5475ad59e 100644 --- a/NetDaemon.sln +++ b/NetDaemon.sln @@ -45,15 +45,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Host.Default", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{A6D03AB9-6C8E-42BE-ACF6-7FA6A1C539C2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Runtime.Tests", "src\Runtime\NetDaemon.Runtime.Tests\NetDaemon.Runtime.Tests.csproj", "{966C5143-7667-4E85-B7E3-336F7C28549F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Runtime.Tests", "src\Runtime\NetDaemon.Runtime.Tests\NetDaemon.Runtime.Tests.csproj", "{966C5143-7667-4E85-B7E3-336F7C28549F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Debug", "Debug", "{E15D4280-7FFC-4F8B-9B8C-CF9AF2BF838C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DebugHost", "dev\DebugHost\DebugHost.csproj", "{898966EA-F814-4B7B-9A3D-5E78C38174B2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugHost", "dev\DebugHost\DebugHost.csproj", "{898966EA-F814-4B7B-9A3D-5E78C38174B2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Extensions.Logging", "src\Extensions\NetDaemon.Extensions.Logging\NetDaemon.Extensions.Logging.csproj", "{00333EBA-DB52-4D56-ADF7-940FB533E530}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Extensions.Logging", "src\Extensions\NetDaemon.Extensions.Logging\NetDaemon.Extensions.Logging.csproj", "{00333EBA-DB52-4D56-ADF7-940FB533E530}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetDaemon.Extensions.Tts", "src\Extensions\NetDaemon.Extensions.Tts\NetDaemon.Extensions.Tts.csproj", "{F4B29B77-9B92-4037-A884-288CA5EF0B78}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Extensions.Tts", "src\Extensions\NetDaemon.Extensions.Tts\NetDaemon.Extensions.Tts.csproj", "{F4B29B77-9B92-4037-A884-288CA5EF0B78}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Extensions.MqttEntityManager", "src\Extensions\NetDaemon.Extensions.MqttEntityManager\NetDaemon.Extensions.MqttEntityManager.csproj", "{3EB8C461-C91E-4900-BFBD-0986CBBE87A6}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -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 + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|x64.ActiveCfg = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|x64.Build.0 = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|x86.ActiveCfg = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Debug|x86.Build.0 = Debug|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Release|Any CPU.Build.0 = Release|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Release|x64.ActiveCfg = Release|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Release|x64.Build.0 = Release|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.Release|x86.ActiveCfg = Release|Any CPU + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6}.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} + {3EB8C461-C91E-4900-BFBD-0986CBBE87A6} = {DFF3E7AA-7A50-4A1E-B3F8-EC01531FB83D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C5FBB7F-654C-4CAC-964F-6D71AF3D62F8} diff --git a/dev/DebugHost/DebugHost.csproj b/dev/DebugHost/DebugHost.csproj index c60fb4bcb..5e89617e8 100644 --- a/dev/DebugHost/DebugHost.csproj +++ b/dev/DebugHost/DebugHost.csproj @@ -28,6 +28,7 @@ + diff --git a/dev/DebugHost/Program.cs b/dev/DebugHost/Program.cs index a581449f6..e850a10e1 100644 --- a/dev/DebugHost/Program.cs +++ b/dev/DebugHost/Program.cs @@ -3,32 +3,35 @@ using NetDaemon.Runtime; using NetDaemon.AppModel; using System.Reflection; +using Microsoft.Extensions.DependencyInjection; using NetDaemon.Extensions.Logging; using NetDaemon.Extensions.Tts; +using NetDaemon.Extensions.MqttEntityManager; #pragma warning disable CA1812 try { await Host.CreateDefaultBuilder(args) - .UseNetDaemonAppSettings() - .UseNetDaemonDefaultLogging() - .UseNetDaemonRuntime() - .UseNetDaemonTextToSpeech() - .ConfigureServices((_, services) => - services - // change type of compilation here - // .AddAppsFromSource(true) - .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) - // Remove this is you are not running the integration! - .AddNetDaemonStateManager() - ) - .Build() - .RunAsync() - .ConfigureAwait(false); + .UseNetDaemonAppSettings() + .UseNetDaemonDefaultLogging() + .UseNetDaemonRuntime() + .UseNetDaemonTextToSpeech() + .UseNetDaemonMqttEntityManagement() + .ConfigureServices((_, services) => + services + // change type of compilation here + // .AddAppsFromSource(true) + .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) + // Remove this is you are not running the integration! + .AddNetDaemonStateManager() + ) + .Build() + .RunAsync() + .ConfigureAwait(false); } catch (Exception e) { Console.WriteLine($"Failed to start host... {e}"); throw; -} +} \ No newline at end of file diff --git a/dev/DebugHost/Properties/launchSettings.json b/dev/DebugHost/Properties/launchSettings.json new file mode 100644 index 000000000..6d8977e7d --- /dev/null +++ b/dev/DebugHost/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "DebugHost": { + "commandName": "Project", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/dev/DebugHost/apps/Extensions/MqttEntityManagerApp.cs b/dev/DebugHost/apps/Extensions/MqttEntityManagerApp.cs new file mode 100644 index 000000000..e2452d3d6 --- /dev/null +++ b/dev/DebugHost/apps/Extensions/MqttEntityManagerApp.cs @@ -0,0 +1,63 @@ +#region + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NetDaemon.AppModel; +using NetDaemon.Extensions.MqttEntityManager; +using NetDaemon.HassModel; + +#endregion + +namespace DebugHost.apps.Extensions; + +[NetDaemonApp] +[Focus] +public class MqttEntityManagerApp : IAsyncInitializable +{ + private readonly IHaContext _ha; + private readonly ILogger _logger; + private readonly IMqttEntityManager _manager; + + public MqttEntityManagerApp(IHaContext ha, ILogger logger, IMqttEntityManager manager) + { + _ha = ha; + _logger = logger; + _manager = manager; + } + + [SuppressMessage("Naming", "CA1727:Use PascalCase for named placeholders", Justification = "")] + [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "We need to log unexpected errors")] + public async Task InitializeAsync(CancellationToken cancellationToken) + { + try + { + _logger.LogInformation("Creating Entity {domain}.{entityId}", "binary_sensor", "manager_test"); + await _manager.CreateAsync("binary_sensor", "manager_test", "motion", "Manager Test").ConfigureAwait(false); + // Using Delay to give Mqtt and HA enough time to process events. + // Only needed for the example as we immediately read the entity and it may not yet exist + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + + var entity = _ha.Entity("binary_sensor.manager_test"); + _logger.LogInformation("Entity {domain}.{entityId} State: {state}", "binary_sensor", "manager_test", entity.State); + + await _manager.UpdateAsync("binary_sensor", "manager_test", "ON", JsonSerializer.Serialize(new { attribute1 = "attr1" })) + .ConfigureAwait(false); + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Entity {domain}.{entityId} State: {state} Attributes: {attributes}", + "binary_sensor", "manager_test", entity.State, entity.Attributes); + + await _manager.RemoveAsync("binary_sensor", "manager_test").ConfigureAwait(false); + await Task.Delay(250, cancellationToken).ConfigureAwait(false); + var removed = _ha.Entity("binary_sensor.manager_test").State == null; + _logger.LogInformation("Removed Entity: {removed}", removed); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + } + } +} \ No newline at end of file diff --git a/dev/DebugHost/appsettings.json b/dev/DebugHost/appsettings.json index f97f74f95..d5a62c8ba 100644 --- a/dev/DebugHost/appsettings.json +++ b/dev/DebugHost/appsettings.json @@ -16,5 +16,8 @@ "NetDaemon": { "Admin": true, "ApplicationConfigurationFolder": "./apps" + }, + "Mqtt": { + "Host": "localhost" } } \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/DependencyInjectionSetup.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/DependencyInjectionSetup.cs new file mode 100644 index 000000000..48d63f647 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/DependencyInjectionSetup.cs @@ -0,0 +1,31 @@ +#region + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using MQTTnet; + +#endregion + +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// DI setup for Mqtt Entity Manager +/// +public static class DependencyInjectionSetup +{ + /// + /// Add support for managing entities via MQTT + /// + /// + /// + public static IHostBuilder UseNetDaemonMqttEntityManagement(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((context, services) => + { + services.AddSingleton(); + services.AddSingleton(); + services.AddTransient(); + services.Configure(context.Configuration.GetSection("Mqtt")); + }); + } +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttConnectionException.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttConnectionException.cs new file mode 100644 index 000000000..5464e5a0b --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttConnectionException.cs @@ -0,0 +1,22 @@ +namespace NetDaemon.Extensions.MqttEntityManager.Exceptions; + +/// +/// MQTT connection failed +/// +public class MqttConnectionException : Exception +{ + /// + /// MQTT connection failed + /// + /// + public MqttConnectionException(string msg) : base(msg) + {} + + /// + /// MQTT connection failed + /// + /// + /// + public MqttConnectionException(string msg, Exception innerException) : base(msg, innerException) + {} +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttPublishException.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttPublishException.cs new file mode 100644 index 000000000..855e48ae1 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/Exceptions/MqttPublishException.cs @@ -0,0 +1,22 @@ +namespace NetDaemon.Extensions.MqttEntityManager.Exceptions; + +/// +/// Failed to publish a message to MQTT +/// +public class MqttPublishException : Exception +{ + /// + /// Failed to publish a message to MQTT + /// + /// + public MqttPublishException(string msg) : base(msg) + {} + + /// + /// Failed to publish a message to MQTT + /// + /// + /// + public MqttPublishException(string msg, Exception innerException) : base(msg, innerException) + {} +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMessageSender.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMessageSender.cs new file mode 100644 index 000000000..33951af4d --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMessageSender.cs @@ -0,0 +1,16 @@ +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// Interface to send messages to MQTT +/// +internal interface IMessageSender +{ + /// + /// Send a message for the given payload to the MQTT topic + /// + /// + /// + /// + /// + Task SendMessageAsync(string topic, string payload, bool retain = false); +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMqttEntityManager.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMqttEntityManager.cs new file mode 100644 index 000000000..faefa0ea9 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/IMqttEntityManager.cs @@ -0,0 +1,24 @@ +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// Interface for managing entities via MQTT +/// +public interface IMqttEntityManager +{ + /// + /// Create an entity in Home Assistant via MQTT + /// + Task CreateAsync(string domain, string entityId, string deviceClass, string name, bool persist = true); + + + /// + /// Remove an entity from Home Assistant + /// + Task RemoveAsync(string domain, string entityId); + + + /// + /// Update state and, optionally, attributes of an HA entity via MQTT + /// + Task UpdateAsync(string domain, string entityId, string state, string? attributes = null); +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSender.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSender.cs new file mode 100644 index 000000000..3a3cd2184 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MessageSender.cs @@ -0,0 +1,101 @@ +#region + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Client.Connecting; +using MQTTnet.Client.Options; +using MQTTnet.Client.Publishing; +using MQTTnet.Exceptions; +using NetDaemon.Extensions.MqttEntityManager.Exceptions; + +#endregion + +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// Manage connections and message publishing to MQTT +/// +internal class MessageSender : IMessageSender +{ + private readonly ILogger _logger; + private readonly MqttConfiguration _mqttConfig; + private readonly IMqttFactory _mqttFactory; + private readonly IMqttClientOptions _mqttClientOptions; + + /// + /// Manage connections and message publishing to MQTT + /// + /// + /// + /// + /// + public MessageSender(ILogger logger, IMqttFactory mqttFactory, IOptions mqttConfig) + { + _logger = logger; + _mqttFactory = mqttFactory; + _mqttConfig = mqttConfig.Value; + + if (string.IsNullOrEmpty(_mqttConfig.Host)) + throw new MqttConfigurationException("The Mqtt config was not found or there was an error loading it. Please add MqttConfiguration section to appsettings.json"); + + _logger.LogDebug("MQTT connection is {host}:{port}/{userId}", _mqttConfig.Host, _mqttConfig.Port, _mqttConfig.UserName); + + _mqttClientOptions = new MqttClientOptionsBuilder() + .WithTcpServer(_mqttConfig.Host, _mqttConfig.Port) + .WithCredentials(_mqttConfig.UserName, _mqttConfig.Password) + .Build(); + } + + /// + /// Connect to MQTT and publish a message to the given topic + /// + /// + /// Json structure of payload + /// + public async Task SendMessageAsync(string topic, string payload, bool retain = false) + { + using var mqttClient = _mqttFactory.CreateMqttClient(); + await ConnectAsync(mqttClient); + await PublishMessage(mqttClient, topic, payload, retain); + } + + private async Task ConnectAsync(IMqttClient mqttClient) + { + try + { + var connectResult = + await mqttClient.ConnectAsync(_mqttClientOptions, CancellationToken.None).ConfigureAwait(false); + if (connectResult.ResultCode != MqttClientConnectResultCode.Success) + throw new MqttConnectionException(connectResult.ReasonString); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw new MqttConnectionException(e.Message, e); + } + } + + private async Task PublishMessage(IApplicationMessagePublisher client, string topic, string payload, bool retain) + { + var message = new MqttApplicationMessageBuilder().WithTopic(topic) + .WithPayload(payload) + .WithRetainFlag(retain) + .Build(); + + _logger.LogDebug("Sending to {topic}: {message}", message.Topic, message.ConvertPayloadToString()); + + try + { + var publishResult = await client.PublishAsync(message, CancellationToken.None).ConfigureAwait(false); + if (publishResult.ReasonCode != MqttClientPublishReasonCode.Success) + throw new MqttPublishException(publishResult.ReasonString); + } + catch (Exception e) + { + _logger.LogError(e.Message, e); + throw new MqttPublishException(e.Message, e); + } + } +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttConfiguration.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttConfiguration.cs new file mode 100644 index 000000000..dff6a7e61 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttConfiguration.cs @@ -0,0 +1,33 @@ +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// Configuration model for MQTT +/// +public class MqttConfiguration +{ + /// + /// Port to connect on, defaults to 1883 + /// + public int Port { get; set; } = 1883; + + /// + /// Discovery Prefix, defaults to "homeassistant" + /// + public string DiscoveryPrefix { get; set; } = "homeassistant"; + + /// + /// Host address of MQTT broker + /// + public string Host { get; set; } = string.Empty; + + /// + /// User name to connect to MQTT broker + /// + public string? UserName { get; set; } + + /// + /// Password to connect to MQTT broker + /// + public string? Password { get; set; } + +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttEntityManager.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttEntityManager.cs new file mode 100644 index 000000000..573f65a05 --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/MqttEntityManager.cs @@ -0,0 +1,80 @@ +#region + +using System.Text.Json; +using Microsoft.Extensions.Options; + +#endregion + +namespace NetDaemon.Extensions.MqttEntityManager; + +/// +/// Manage entities via MQTT +/// +internal class MqttEntityManager : IMqttEntityManager +{ + private readonly MqttConfiguration _config; + private readonly IMessageSender _messageSender; + + /// + /// Manage entities via MQTT + /// + /// + /// + public MqttEntityManager(IMessageSender messageSender, IOptions config) + { + _messageSender = messageSender; + _config = config.Value; + } + + /// + /// Create an entity in Home Assistant via MQTT + /// + /// + /// + /// + /// + /// This will persist the entity if Home Assistant restarts + public async Task CreateAsync(string domain, string entityId, string deviceClass, string name, bool persist = true) + { + var payload = JsonSerializer.Serialize(new + { + name, + device_class = deviceClass, + state_topic = StatePath(domain, entityId), + json_attributes_topic = AttrsPath(domain, entityId) + }); + await _messageSender.SendMessageAsync(ConfigPath(domain, entityId), payload, persist).ConfigureAwait(false); + } + + /// + /// Remove an entity from Home Assistant + /// + /// + /// + public async Task RemoveAsync(string domain, string entityId) + { + await _messageSender.SendMessageAsync(ConfigPath(domain, entityId), string.Empty).ConfigureAwait(false); + } + + /// + /// Update state and, optionally, attributes of an HA entity via MQTT + /// + /// + /// + /// + /// Json string of attributes + public async Task UpdateAsync(string domain, string entityId, string state, string? attributes = null) + { + await _messageSender.SendMessageAsync(StatePath(domain, entityId), state).ConfigureAwait(false); + if (attributes != null) + await _messageSender.SendMessageAsync(AttrsPath(domain, entityId), attributes).ConfigureAwait(false); + } + + private string AttrsPath(string domain, string entityId) => $"{RootPath(domain, entityId)}/attributes"; + + private string ConfigPath(string domain, string entityId) => $"{RootPath(domain, entityId)}/config"; + + private string RootPath(string domain, string entityId) => $"{_config.DiscoveryPrefix}/{domain}/{entityId}"; + + private string StatePath(string domain, string entityId) => $"{RootPath(domain, entityId)}/state"; +} \ No newline at end of file diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/NetDaemon.Extensions.MqttEntityManager.csproj b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/NetDaemon.Extensions.MqttEntityManager.csproj new file mode 100644 index 000000000..31203f28f --- /dev/null +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/NetDaemon.Extensions.MqttEntityManager.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + enable + enable + 10.0 + + NetDaemon + JoySoftware.NetDaemon.Extensions.Mqtt + 22.5.1 + eugeneniemand, skotl + JoySoftware + Mqtt extensions for NetDaemon + https://github.com/net-daemon/netdaemon + MIT + + This package contains the features to manage entities using MQTT for NetDaemon. + + Please advice this is still in beta + Home Assistant, NetDaemon, MQTT + true + + + + + + + + + + + diff --git a/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj b/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj index cc6db3b6f..70ea97319 100644 --- a/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj +++ b/src/Host/NetDaemon.Host.Default/NetDaemon.Host.Default.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -27,11 +27,12 @@ - - - - - + + + + + + ..\..\..\.linting\roslynator.ruleset diff --git a/src/Host/NetDaemon.Host.Default/Program.cs b/src/Host/NetDaemon.Host.Default/Program.cs index 74ee1954f..b7a732ced 100644 --- a/src/Host/NetDaemon.Host.Default/Program.cs +++ b/src/Host/NetDaemon.Host.Default/Program.cs @@ -7,6 +7,7 @@ using NetDaemon.Extensions.Tts; using NetDaemon.Runtime; using NetDaemon.HassModel.Integration; +using NetDaemon.Extensions.MqttEntityManager; #pragma warning disable CA1812 // This assembly is not used by the host so we have to force load it @@ -20,6 +21,7 @@ await Host.CreateDefaultBuilder(args) .UseNetDaemonDefaultLogging() .UseNetDaemonRuntime() .UseNetDaemonTextToSpeech() + .UseNetDaemonMqttEntityManagement() .ConfigureServices((_, services) => services .AddAppsFromSource()