diff --git a/.gitignore b/.gitignore
index 3e81da9ef..058672679 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,5 @@
TestResults
lcov.info
codecover
-**.DS_Store
\ No newline at end of file
+**.DS_Store
+.generated
\ No newline at end of file
diff --git a/src/App/NetDaemon.App/NetDaemon.App.csproj b/src/App/NetDaemon.App/NetDaemon.App.csproj
index c6dce0505..0ef9b12b7 100644
--- a/src/App/NetDaemon.App/NetDaemon.App.csproj
+++ b/src/App/NetDaemon.App/NetDaemon.App.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs
index c75548a94..fea344e6a 100644
--- a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs
+++ b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs
@@ -55,7 +55,12 @@ public class NetDaemonHost : INetDaemonHost
{
"light",
"switch",
- "input_boolean"
+ "input_boolean",
+ "automation",
+ "input_boolean",
+ "camera",
+ "scene",
+ "script",
};
private bool _stopped;
diff --git a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj
index d00498a59..3bb73d03b 100644
--- a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj
+++ b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj
@@ -20,7 +20,7 @@
-
+
diff --git a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj
index f27781da1..55e6e21e6 100644
--- a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj
+++ b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj
@@ -21,8 +21,8 @@
-
-
+
+
diff --git a/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs b/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs
new file mode 100644
index 000000000..ed094c93e
--- /dev/null
+++ b/src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs
@@ -0,0 +1,108 @@
+using JoySoftware.HomeAssistant.NetDaemon.DaemonRunner.Service.Config;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("NetDaemon.Daemon.Tests")]
+
+namespace JoySoftware.HomeAssistant.NetDaemon.DaemonRunner.Service.App
+{
+ public class CodeGenerator
+ {
+ ///
+ /// Mapps the domain to corresponding implemented Fluent API, will be added as
+ /// more and more entity types are supported
+ ///
+ private static IDictionary _FluentApiMapper = new Dictionary
+ {
+ ["light"] = ("Entity", "IEntity"),
+ ["script"] = ("Entity", "IEntity"),
+ ["scene"] = ("Entity", "IEntity"),
+ ["switch"] = ("Entity", "IEntity"),
+ ["camera"] = ("Entity", "IEntity"),
+ ["media_player"] = ("MediaPlayer", "IMediaPlayer"),
+ ["automation"] = ("Entity", "IEntity"),
+ // ["input_boolean"],
+ // ["remote"],
+ // ["climate"],
+ };
+
+ public string? GenerateCode(string nameSpace, IEnumerable entities)
+ {
+ var code = SyntaxFactory.CompilationUnit();
+
+ // Add Usings statements
+ code = code.AddUsings(SyntaxFactory.UsingDirective(SyntaxFactory.ParseName("JoySoftware.HomeAssistant.NetDaemon.Common")));
+
+ // Add namespace
+ var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(nameSpace)).NormalizeWhitespace();
+
+ // Add support for extensions for entities
+ var extensionClass = SyntaxFactory.ClassDeclaration("EntityExtension");
+
+ extensionClass = extensionClass.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword),
+ SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword));
+
+
+ // Get all available domains, this is used to create the extensionmethods
+ var domains = GetDomainsFromEntities(entities);
+
+ foreach (var domain in domains)
+ {
+ if (_FluentApiMapper.ContainsKey(domain))
+ {
+ var camelCaseDomain = domain.ToCamelCase();
+ var method = $@"public static {camelCaseDomain}Entities {camelCaseDomain}Ex(this NetDaemonApp app) => new {camelCaseDomain}Entities(app);";
+ var methodDeclaration = CSharpSyntaxTree.ParseText(method).GetRoot().ChildNodes().OfType().FirstOrDefault();
+ extensionClass = extensionClass.AddMembers(methodDeclaration);
+ }
+
+ }
+ namespaceDeclaration = namespaceDeclaration.AddMembers(extensionClass);
+
+ // Add the classes implementing the specific entities
+ foreach (var domain in GetDomainsFromEntities(entities))
+ {
+ if (_FluentApiMapper.ContainsKey(domain))
+ {
+ var classDeclaration = $@"public partial class {domain.ToCamelCase()}Entities
+ {{
+ private readonly NetDaemonApp _app;
+
+ public {domain.ToCamelCase()}Entities(NetDaemonApp app)
+ {{
+ _app = app;
+ }}
+ }}";
+ var entityClass = CSharpSyntaxTree.ParseText(classDeclaration).GetRoot().ChildNodes().OfType().FirstOrDefault();
+ foreach (var entity in entities.Where(n => n.StartsWith(domain)))
+ {
+ var (fluent, fluentInterface) = _FluentApiMapper[domain];
+
+ var propertyCode = $@"public {fluentInterface} {entity[(entity.IndexOf(".") + 1)..].ToCamelCase()} => _app.{fluent}(""{entity}"");";
+ var propDeclaration = CSharpSyntaxTree.ParseText(propertyCode).GetRoot().ChildNodes().OfType().FirstOrDefault();
+ entityClass = entityClass.AddMembers(propDeclaration);
+
+ }
+ namespaceDeclaration = namespaceDeclaration.AddMembers(entityClass);
+
+ }
+ }
+
+ code = code.AddMembers(namespaceDeclaration);
+
+ return code.NormalizeWhitespace(indentation: " ", eol: "\n").ToFullString();
+ }
+
+ ///
+ /// Returns a list of domains from all entities
+ ///
+ /// A list of entities
+ internal static IEnumerable GetDomainsFromEntities(IEnumerable entities) =>
+ entities.Select(n => n[0..n.IndexOf(".")]).Distinct();
+
+ }
+}
\ No newline at end of file
diff --git a/src/DaemonRunner/DaemonRunner/Service/HostConfig.cs b/src/DaemonRunner/DaemonRunner/Service/HostConfig.cs
index 76bb7c411..74728cb44 100644
--- a/src/DaemonRunner/DaemonRunner/Service/HostConfig.cs
+++ b/src/DaemonRunner/DaemonRunner/Service/HostConfig.cs
@@ -26,5 +26,9 @@ public HostConfig()
[JsonPropertyName("source_folder")]
public string? SourceFolder { get; set; } = null;
+
+ [JsonPropertyName("generate_entities")]
+ public bool? GenerateEntitiesOnStartup { get; set; } = false;
+
}
}
\ No newline at end of file
diff --git a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs
index a561589b4..528eddd23 100644
--- a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs
+++ b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs
@@ -84,6 +84,23 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
+ // Generate code if requested
+ var envGenEntities = Environment.GetEnvironmentVariable("HASS_GEN_ENTITIES");
+ if (envGenEntities is object)
+ {
+ if (envGenEntities == "True")
+ {
+ var codeGen = new CodeGenerator();
+ var source = codeGen.GenerateCode("Netdaemon.Generated.Extensions",
+ _daemonHost.State.Select(n => n.EntityId).Distinct());
+ var genDirectory = System.IO.Path.Combine(sourceFolder, ".generated");
+
+ if (!System.IO.Directory.Exists(genDirectory))
+ System.IO.Directory.CreateDirectory(genDirectory);
+
+ System.IO.File.WriteAllText(System.IO.Path.Combine(genDirectory, "EntityExtensions.cs"), source);
+ }
+ }
using (var codeManager = new CodeManager(sourceFolder, _daemonHost.Logger))
{
await codeManager.EnableApplicationDiscoveryServiceAsync(_daemonHost, discoverServicesOnStartup: true);
diff --git a/src/Service/.config/hassio_config.json b/src/Service/.config/hassio_config.json
index 2b31de8fd..af6f9f31e 100644
--- a/src/Service/.config/hassio_config.json
+++ b/src/Service/.config/hassio_config.json
@@ -1,3 +1,4 @@
{
- "log_level": "trace"
+ "log_level": "trace",
+ "generate_entities": true
}
\ No newline at end of file
diff --git a/src/Service/HassioConfig.cs b/src/Service/HassioConfig.cs
new file mode 100644
index 000000000..77c01d564
--- /dev/null
+++ b/src/Service/HassioConfig.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+
+public class HassioConfig
+{
+
+ [JsonPropertyName("log_level")]
+ public string? LogLevel { get; set; }
+
+ [JsonPropertyName("generate_entities")]
+ public bool? GenerateEntitiesOnStart { get; set; }
+}
\ No newline at end of file
diff --git a/src/Service/Program.cs b/src/Service/Program.cs
index fa8d1a5c2..06e9230a7 100644
--- a/src/Service/Program.cs
+++ b/src/Service/Program.cs
@@ -11,6 +11,7 @@ namespace Service
{
internal class Program
{
+ // private const string _hassioConfigPath = "/root/src/src/Service/.config/hassio_config.json";
private const string _hassioConfigPath = "data/options.json";
private static LogLevel LogLevel = LogLevel.Trace;
@@ -21,11 +22,11 @@ public static async Task Main(string[] args)
///
if (File.Exists(_hassioConfigPath))
{
- var hassAddOnSettings = await JsonSerializer.DeserializeAsync>(
+ var hassAddOnSettings = await JsonSerializer.DeserializeAsync(
File.OpenRead(_hassioConfigPath)).ConfigureAwait(false);
- if (hassAddOnSettings.ContainsKey("log_level"))
+ if (hassAddOnSettings.LogLevel is object)
{
- Program.LogLevel = hassAddOnSettings["log_level"] switch
+ Program.LogLevel = hassAddOnSettings.LogLevel switch
{
"info" => LogLevel.Information,
"debug" => LogLevel.Debug,
@@ -35,6 +36,10 @@ public static async Task Main(string[] args)
_ => LogLevel.Information
};
}
+ if (hassAddOnSettings.GenerateEntitiesOnStart is object)
+ {
+ Environment.SetEnvironmentVariable("HASS_GEN_ENTITIES", hassAddOnSettings.GenerateEntitiesOnStart.ToString());
+ }
}
}
catch (Exception)
diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs
new file mode 100644
index 000000000..ba55b5bd2
--- /dev/null
+++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs
@@ -0,0 +1,53 @@
+using System.Linq;
+using JoySoftware.HomeAssistant.NetDaemon.DaemonRunner.Service.App;
+using Xunit;
+
+namespace NetDaemon.Daemon.Tests.DaemonRunner.App
+{
+ public class CodeGenerationTests
+ {
+ [Fact]
+ public void TestGenerateCode()
+ {
+ // ARRANGE
+ var x = new CodeGenerator();
+ // ACT
+ var code = x.GenerateCode("Netdaemon.Generated.Extensions", new string[] { "light.koket_fonster", "media_player.my_player" });
+
+ // ASSERT
+
+ Assert.Equal(System.IO.File.ReadAllText("DaemonRunner/CodeGen/CodeGenTestFixture.cs"), code);
+ // System.IO.File.WriteAllText("/root/src/tests/NetDaemon.Daemon.Tests/DaemonRunner/CodeGen/CodeGenTestFixture.cs", code);
+ }
+
+ [Fact]
+ public void WhenGivenAnArrayOfEntitiesTheDomainShouldReturnCorrectDomains()
+ {
+ // ARRANGE
+ var entities = new string[]
+ {
+ "light.the_light",
+ "light.kitchen",
+ "media_player.player",
+ "scene.thescene",
+ "switch.myswitch",
+ "switch.myswitch2",
+ "camera.acamera",
+ "automation.wowautomation",
+ "script.myscript"
+ };
+ // ACT
+ var domainsInCamelCase = CodeGenerator.GetDomainsFromEntities(entities);
+ // ASSERT
+ Assert.Equal(7, domainsInCamelCase.Count());
+ Assert.Collection(domainsInCamelCase,
+ n => Assert.Equal("light", n),
+ n => Assert.Equal("media_player", n),
+ n => Assert.Equal("scene", n),
+ n => Assert.Equal("switch", n),
+ n => Assert.Equal("camera", n),
+ n => Assert.Equal("automation", n),
+ n => Assert.Equal("script", n));
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/CodeGen/CodeGenTestFixture.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/CodeGen/CodeGenTestFixture.cs
new file mode 100644
index 000000000..344db30ca
--- /dev/null
+++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/CodeGen/CodeGenTestFixture.cs
@@ -0,0 +1,32 @@
+using JoySoftware.HomeAssistant.NetDaemon.Common;
+
+namespace Netdaemon.Generated.Extensions
+{
+ public static partial class EntityExtension
+ {
+ public static LightEntities LightEx(this NetDaemonApp app) => new LightEntities(app);
+ public static MediaPlayerEntities MediaPlayerEx(this NetDaemonApp app) => new MediaPlayerEntities(app);
+ }
+
+ public partial class LightEntities
+ {
+ private readonly NetDaemonApp _app;
+ public LightEntities(NetDaemonApp app)
+ {
+ _app = app;
+ }
+
+ public IEntity KoketFonster => _app.Entity("light.koket_fonster");
+ }
+
+ public partial class MediaPlayerEntities
+ {
+ private readonly NetDaemonApp _app;
+ public MediaPlayerEntities(NetDaemonApp app)
+ {
+ _app = app;
+ }
+
+ public IMediaPlayer MyPlayer => _app.MediaPlayer("media_player.my_player");
+ }
+}
\ No newline at end of file
diff --git a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs
index 184df7299..2e36c9803 100644
--- a/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs
+++ b/tests/NetDaemon.Daemon.Tests/DaemonRunner/Config/ConfigTest.cs
@@ -108,6 +108,7 @@ public void JSonSerializeShouldBeCorrectForConfig()
Host = "host",
Port = 1234,
Ssl = true,
+ GenerateEntitiesOnStartup = false,
SourceFolder = "somefolder"
};
@@ -119,6 +120,7 @@ public void JSonSerializeShouldBeCorrectForConfig()
Assert.Contains("port", obj);
Assert.Contains("ssl", obj);
Assert.Contains("source_folder", obj);
+ Assert.Contains("generate_entities", obj);
}
diff --git a/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj b/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj
index f25e2ea0d..8aecf36f2 100644
--- a/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj
+++ b/tests/NetDaemon.Daemon.Tests/NetDaemon.Daemon.Tests.csproj
@@ -33,6 +33,7 @@
+
@@ -40,6 +41,9 @@
Always
+
+ Always
+
Always