From 25a0b1549db112795f78c8239203c095a6888c6c Mon Sep 17 00:00:00 2001 From: helto4real Date: Fri, 3 Apr 2020 22:58:13 +0200 Subject: [PATCH] Generate entities on startup --- .gitignore | 3 +- src/App/NetDaemon.App/NetDaemon.App.csproj | 2 +- .../NetDaemon.Daemon/Daemon/NetDaemonHost.cs | 7 +- .../NetDaemon.Daemon/NetDaemon.Daemon.csproj | 2 +- .../DaemonRunner/DaemonRunner.csproj | 4 +- .../DaemonRunner/Service/App/CodeGenerator.cs | 108 ++++++++++++++++++ .../DaemonRunner/Service/HostConfig.cs | 4 + .../DaemonRunner/Service/RunnerService.cs | 17 +++ src/Service/.config/hassio_config.json | 3 +- src/Service/HassioConfig.cs | 11 ++ src/Service/Program.cs | 11 +- .../DaemonRunner/App/CodeGenTests.cs | 53 +++++++++ .../CodeGen/CodeGenTestFixture.cs | 32 ++++++ .../DaemonRunner/Config/ConfigTest.cs | 2 + .../NetDaemon.Daemon.Tests.csproj | 4 + 15 files changed, 253 insertions(+), 10 deletions(-) create mode 100644 src/DaemonRunner/DaemonRunner/Service/App/CodeGenerator.cs create mode 100644 src/Service/HassioConfig.cs create mode 100644 tests/NetDaemon.Daemon.Tests/DaemonRunner/App/CodeGenTests.cs create mode 100644 tests/NetDaemon.Daemon.Tests/DaemonRunner/CodeGen/CodeGenTestFixture.cs 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