diff --git a/NetDaemon.sln b/NetDaemon.sln index 5475ad59e..65c5e2dad 100644 --- a/NetDaemon.sln +++ b/NetDaemon.sln @@ -49,7 +49,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Runtime.Tests", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Debug", "Debug", "{E15D4280-7FFC-4F8B-9B8C-CF9AF2BF838C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugHost", "dev\DebugHost\DebugHost.csproj", "{898966EA-F814-4B7B-9A3D-5E78C38174B2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DebugHost", "src\debug\DebugHost\DebugHost.csproj", "{898966EA-F814-4B7B-9A3D-5E78C38174B2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Extensions.Logging", "src\Extensions\NetDaemon.Extensions.Logging\NetDaemon.Extensions.Logging.csproj", "{00333EBA-DB52-4D56-ADF7-940FB533E530}" EndProject @@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetDaemon.Extensions.Tts", 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DebugWebHost", "src\debug\DebugWebHost\DebugWebHost.csproj", "{AEBC7828-7C19-4A86-B6E2-58B5171347B1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -271,6 +273,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 + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|x64.ActiveCfg = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|x64.Build.0 = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Debug|x86.Build.0 = Debug|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Release|Any CPU.Build.0 = Release|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Release|x64.ActiveCfg = Release|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Release|x64.Build.0 = Release|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.Release|x86.ActiveCfg = Release|Any CPU + {AEBC7828-7C19-4A86-B6E2-58B5171347B1}.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 @@ -307,6 +321,7 @@ Global {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} + {AEBC7828-7C19-4A86-B6E2-58B5171347B1} = {E15D4280-7FFC-4F8B-9B8C-CF9AF2BF838C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7C5FBB7F-654C-4CAC-964F-6D71AF3D62F8} diff --git a/dev/DebugHost/apps/HelloApp/HelloApp.cs b/dev/DebugHost/apps/HelloApp/HelloApp.cs deleted file mode 100644 index ccef477dc..000000000 --- a/dev/DebugHost/apps/HelloApp/HelloApp.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Reactive.Linq; -using Microsoft.Extensions.Logging; -using NetDaemon.AppModel; -using NetDaemon.HassModel; - -namespace Apps; - -[NetDaemonApp] -// [Focus] -public class HelloApp -{ - public HelloApp(IHaContext ha, ILogger logger) - { - // .Where(n => n.EventType == "test_event") - ha?.Events.Where(n => n.EventType == "test_event").Subscribe( n => - { - logger.LogInformation("Hello testevent"); - }); - ha?.CallService("notify", "persistent_notification", data: new { message = "Notify me", title = "Hello world!" }); - } -} \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs index ac2724139..ad30aaa8d 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs @@ -66,7 +66,7 @@ public async Task TestGetApplicationsLocalWithDisabled() var fakeStateManager = (FakeAppStateManager?) builder.Services.GetService(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); // ACT var apps = appModelContext.Applications; @@ -105,7 +105,7 @@ public async Task TestGetApplicationsLocalWithEnabled() .Build(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); // ACT var apps = appModelContext.Applications; @@ -176,7 +176,7 @@ public async Task TestGetApplicationsShouldReturnNonErrorOnes() // ACT var loadApps = (await appModel! - .InitializeAsync(CancellationToken.None)).Applications; + .LoadNewApplicationContext(CancellationToken.None)).Applications; // CHECK @@ -266,7 +266,7 @@ public async Task TestFocusShouldAlwaysLoadAppIfIndependentOfStateManager(Applic .Build(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); // ACT var apps = appModelContext.Applications; @@ -300,7 +300,7 @@ public async Task TestInjectedClassShouldHaveCorrectValue() .Build(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); // ACT var apps = appModelContext.Applications; diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs index d64f0be5c..ebee0a097 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Config/ConfigTests.cs @@ -131,7 +131,7 @@ public async Task TestAddYamlConfigWithTypeConverterGetsSettingsCorrectly2() var appModel = scope.ServiceProvider.GetService(); // ACT - var loadApps = (await appModel!.InitializeAsync(CancellationToken.None)).Applications; + var loadApps = (await appModel!.LoadNewApplicationContext(CancellationToken.None)).Applications; var application = (Application)loadApps.First(n => n.Id == "LocalApps.MyAppLocalApp"); var app = (MyAppLocalApp?)application?.ApplicationContext?.Instance; // CHECK diff --git a/src/AppModel/NetDaemon.AppModel.Tests/Helpers/TestHelpers.cs b/src/AppModel/NetDaemon.AppModel.Tests/Helpers/TestHelpers.cs index 96f305e78..f384c5007 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/Helpers/TestHelpers.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/Helpers/TestHelpers.cs @@ -21,7 +21,7 @@ internal static async Task> GetLocalApplicatio }) .Build(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); return appModelContext.Applications; } @@ -43,7 +43,7 @@ internal static async Task> GetDynamicApplicat }) .Build(); var appModel = builder.Services.GetService(); - var appModelContext = await appModel!.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + var appModelContext = await appModel!.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); return appModelContext.Applications; } } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Common/IAppModel.cs b/src/AppModel/NetDaemon.AppModel/Common/IAppModel.cs index 3b7f9d375..251f6cc35 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/IAppModel.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/IAppModel.cs @@ -3,7 +3,7 @@ namespace NetDaemon.AppModel; /// /// Application model /// -public interface IAppModel : IAsyncDisposable +public interface IAppModel { /// /// Instance and configure all applications. @@ -15,5 +15,5 @@ public interface IAppModel : IAsyncDisposable /// Depending on the selected compilation type it will be local or dynamically compiled apps /// /// - Task InitializeAsync(CancellationToken cancellationToken); + Task LoadNewApplicationContext(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Common/IAppModelContext.cs b/src/AppModel/NetDaemon.AppModel/Common/IAppModelContext.cs index 1ea312375..da7b25007 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/IAppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/IAppModelContext.cs @@ -3,7 +3,7 @@ namespace NetDaemon.AppModel; /// /// Manage AppModel state and lifecycle /// -public interface IAppModelContext : IAsyncDisposable +public interface IAppModelContext : IAsyncInitializable, IAsyncDisposable { /// /// Current instantiated and running applications diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs index b1852adf7..7a5433b9d 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs @@ -1,29 +1,22 @@ namespace NetDaemon.AppModel.Internal; +/// +/// This class serves as a factory for creating and initializing new ApplicationContexts +/// internal class AppModelImpl : IAppModel { private readonly IServiceProvider _provider; - public AppModelImpl( - IServiceProvider provider - ) + public AppModelImpl(IServiceProvider provider) { _provider = provider; } - internal IAppModelContext? CurrentContext { get; set; } - - public async ValueTask DisposeAsync() - { - if (CurrentContext is not null) - await CurrentContext.DisposeAsync().ConfigureAwait(false); - } - - public async Task InitializeAsync(CancellationToken cancellationToken) + public async Task LoadNewApplicationContext(CancellationToken cancellationToken) { + // Create a new AppModelContext var appModelContext = _provider.GetRequiredService(); - var initContext = (IAsyncInitializable)appModelContext; - await initContext.InitializeAsync(CancellationToken.None); + await appModelContext.InitializeAsync(cancellationToken); return appModelContext; } } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs index f6fe4938d..567723c7f 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs @@ -1,9 +1,8 @@ -using System.Reflection; using NetDaemon.AppModel.Internal.AppFactoryProviders; namespace NetDaemon.AppModel.Internal; -internal class AppModelContext : IAppModelContext, IAsyncInitializable +internal class AppModelContext : IAppModelContext { private readonly List _applications = new(); @@ -37,11 +36,14 @@ public async Task InitializeAsync(CancellationToken cancellationToken) public async ValueTask DisposeAsync() { - if (_isDisposed) - return; + if (_isDisposed) return; + _isDisposed = true; - foreach (var appInstance in _applications) await appInstance.DisposeAsync().ConfigureAwait(false); + foreach (var appInstance in _applications) + { + await appInstance.DisposeAsync().ConfigureAwait(false); + } + _applications.Clear(); - _isDisposed = true; } } \ No newline at end of file diff --git a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs index 54264e694..854b1f02f 100644 --- a/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs +++ b/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs @@ -74,7 +74,7 @@ private async Task InternalRunAsync(string host, int port, bool ssl, string toke { if (isRetry) { - _logger.LogDebug("Client disconnected, retrying in {seconds} seconds...", timeout.TotalSeconds); + _logger.LogDebug("Client disconnected, retrying in {Seconds} seconds...", timeout.TotalSeconds); // This is a retry await Task.Delay(timeout, combinedToken.Token).ConfigureAwait(false); } diff --git a/src/Extensions/NetDaemon.Extensions.Logging/Internal/SerilogConfiguratior.cs b/src/Extensions/NetDaemon.Extensions.Logging/Internal/SerilogConfiguratior.cs index e19840661..f8aa4e239 100644 --- a/src/Extensions/NetDaemon.Extensions.Logging/Internal/SerilogConfiguratior.cs +++ b/src/Extensions/NetDaemon.Extensions.Logging/Internal/SerilogConfiguratior.cs @@ -28,6 +28,7 @@ public static LoggerConfiguration Configure(LoggerConfiguration loggerConfigurat .MinimumLevel.Override("System.Net.Http.HttpClient", LogEventLevel.Warning) .Enrich.FromLogContext() .WriteTo.Console( + formatProvider: CultureInfo.InvariantCulture, theme: NetDaemonLoggingThemes.NetDaemonConsoleThemes.GetThemeByType(loggingConfiguration .ConsoleThemeType), applyThemeToRedirectedOutput: true, diff --git a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/AssuredMqttConnection.cs b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/AssuredMqttConnection.cs index 7f914f1bc..66be73ea6 100644 --- a/src/Extensions/NetDaemon.Extensions.MqttEntityManager/AssuredMqttConnection.cs +++ b/src/Extensions/NetDaemon.Extensions.MqttEntityManager/AssuredMqttConnection.cs @@ -71,7 +71,7 @@ private async Task ConnectAsync(MqttConfiguration mqttConfig, IMqttFactoryWrappe private Task MqttClientOnDisconnectedAsync(MqttClientDisconnectedEventArgs arg) { - _logger.LogDebug("MQTT disconnected: {Reason}", arg.ConnectResult.ReasonString); + _logger.LogDebug("MQTT disconnected: {Reason}", arg.ConnectResult?.ReasonString); return Task.CompletedTask; } diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Helpers/HomeAssistantRunnerMock.cs b/src/Runtime/NetDaemon.Runtime.Tests/Helpers/HomeAssistantRunnerMock.cs index c37bc4084..9bb9f6644 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Helpers/HomeAssistantRunnerMock.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Helpers/HomeAssistantRunnerMock.cs @@ -8,7 +8,7 @@ internal class HomeAssistantRunnerMock : Mock public HomeAssistantClientMock ClientMock { get; } public Subject ConnectMock { get; } public Subject DisconnectMock { get; } - public HomeAssistantRunnerMock(CancellationToken cancelToken) + public HomeAssistantRunnerMock() { ConnectMock = new(); DisconnectMock = new(); @@ -25,12 +25,14 @@ public HomeAssistantRunnerMock(CancellationToken cancelToken) It.IsAny(), It.IsAny(), It.IsAny())).Returns( - async () => + async (string _, int _, bool _, string _, string _, TimeSpan _, CancellationToken ct) => { - await Task.Delay(-1, cancelToken); + await Task.Delay(-1, ct); } ); } + + public void MockConnect() => ConnectMock.OnNext(ClientMock.ConnectionMock.Object); } internal class HomeAssistantClientMock : Mock diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs index dc9923db4..c68c10ebb 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Integration/TestRuntime.cs @@ -3,7 +3,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using NetDaemon.AppModel; -using NetDaemon.Infrastructure.ObservableHelpers; using NetDaemon.Runtime.Internal; using NetDaemon.Runtime.Tests.Helpers; @@ -15,19 +14,18 @@ public class TestRuntime public async Task TestApplicationIsLoaded() { var timedCancellationSource = new CancellationTokenSource(5000); - var haRunner = new HomeAssistantRunnerMock(timedCancellationSource.Token); + var haRunner = new HomeAssistantRunnerMock(); - var hostBuilder = GetDefaultHostBuilder("Fixtures"); + var hostBuilder = GetDefaultHostBuilder(); var host = hostBuilder.ConfigureServices((_, services) => services .AddSingleton(haRunner.Object) .AddAppsFromAssembly(Assembly.GetExecutingAssembly()) ).Build(); + var runnerTask = host.RunAsync(timedCancellationSource.Token); - var runnerTask = host.RunAsync(); - - haRunner.ConnectMock.OnNext(haRunner.ClientMock.ConnectionMock.Object); + haRunner.MockConnect(); var service = (NetDaemonRuntime) host.Services.GetService()!; var instances = service.ApplicationInstances; @@ -40,9 +38,9 @@ public async Task TestApplicationIsLoaded() public async Task TestApplicationReactToNewEvents() { var timedCancellationSource = new CancellationTokenSource(-1); - var haRunner = new HomeAssistantRunnerMock(timedCancellationSource.Token); + var haRunner = new HomeAssistantRunnerMock(); - var hostBuilder = GetDefaultHostBuilder("Fixtures"); + var hostBuilder = GetDefaultHostBuilder(); var host = hostBuilder.ConfigureServices((_, services) => services .AddSingleton(haRunner.Object) @@ -50,10 +48,10 @@ public async Task TestApplicationReactToNewEvents() .AddTransient>(_ => haRunner.ClientMock.ConnectionMock.HomeAssistantEventMock) ).Build(); - var runnerTask = host.RunAsync(); - while (!haRunner.ConnectMock.HasObservers && !runnerTask.IsCompleted) await Task.Delay(10); - haRunner.ConnectMock.OnNext(haRunner.ClientMock.ConnectionMock.Object); + var runnerTask = host.StartAsync(timedCancellationSource.Token); + haRunner.MockConnect(); _ = (NetDaemonRuntime) host.Services.GetService()!; + await runnerTask.ConfigureAwait(false); haRunner.ClientMock.ConnectionMock.AddStateChangeEvent( new HassState @@ -66,30 +64,29 @@ public async Task TestApplicationReactToNewEvents() EntityId = "binary_sensor.mypir", State = "on" }); - + // stopping the host will also flush any event queues + await host.StopAsync(timedCancellationSource.Token).ConfigureAwait(false); + haRunner.ClientMock.ConnectionMock.Verify( - n => n.SendCommandAsync(It.IsAny(), + n => n.SendCommandAsync(It.IsAny(), It.IsAny()), Times.Once); - timedCancellationSource.Cancel(); - await runnerTask.ConfigureAwait(false); } [Fact] public async Task TestApplicationReactToNewEventsAndThrowException() { var timedCancellationSource = new CancellationTokenSource(5000); - var haRunner = new HomeAssistantRunnerMock(timedCancellationSource.Token); + var haRunner = new HomeAssistantRunnerMock(); - var hostBuilder = GetDefaultHostBuilder("Fixtures"); + var hostBuilder = GetDefaultHostBuilder(); var host = hostBuilder.ConfigureServices((_, services) => services .AddSingleton(haRunner.Object) .AddNetDaemonApp() ).Build(); - var runnerTask = host.RunAsync(); - while (!haRunner.ConnectMock.HasObservers && !runnerTask.IsCompleted) await Task.Delay(10); - haRunner.ConnectMock.OnNext(haRunner.ClientMock.ConnectionMock.Object); + var runnerTask = host.StartAsync(timedCancellationSource.Token); + haRunner.MockConnect(); _ = (NetDaemonRuntime) host.Services.GetService()!; haRunner.ClientMock.ConnectionMock.AddStateChangeEvent( @@ -107,29 +104,37 @@ public async Task TestApplicationReactToNewEventsAndThrowException() timedCancellationSource.Cancel(); await runnerTask.ConfigureAwait(false); } - - private static IHostBuilder GetDefaultHostBuilder(string path) + + + [Fact] + public async Task TestShutdownHostShutDownApps() { - return Host.CreateDefaultBuilder() - .UseNetDaemonAppSettings() + var timedCancellationSource = new CancellationTokenSource(); + var haRunner = new HomeAssistantRunnerMock(); + + var disposableApp = new Mock(); + + var host = Host.CreateDefaultBuilder() .UseNetDaemonRuntime() .ConfigureServices((_, services) => - { - services.Configure(hostOptions => - { - hostOptions.BackgroundServiceExceptionBehavior = BackgroundServiceExceptionBehavior.Ignore; - }); - services.AddTransient>( - _ => new FakeOptions(Path.Combine(AppContext.BaseDirectory, path))); - services.AddScoped>(); - services.AddScoped>(s => - s.GetRequiredService>()); - }) - .ConfigureAppConfiguration((_, config) => - { - config.AddYamlAppConfig( - Path.Combine(AppContext.BaseDirectory, - path)); - }); + services + .AddSingleton(haRunner.Object) + .AddNetDaemonApp(_ => disposableApp.Object)) + .Build(); + var runnerTask = host.StartAsync(timedCancellationSource.Token); + + haRunner.MockConnect(); + await runnerTask.WaitAsync(timedCancellationSource.Token).ConfigureAwait(false);; + + await host.StopAsync(timedCancellationSource.Token).WaitAsync(timedCancellationSource.Token).ConfigureAwait(false); + host.Dispose(); + + disposableApp.Verify(m=>m.DisposeAsync(), Times.Once); + } + + private static IHostBuilder GetDefaultHostBuilder() + { + return Host.CreateDefaultBuilder() + .UseNetDaemonRuntime(); } } diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Internal/NetDaemonRuntimeTests.cs b/src/Runtime/NetDaemon.Runtime.Tests/Internal/NetDaemonRuntimeTests.cs index d1e08ea63..60b937d26 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Internal/NetDaemonRuntimeTests.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Internal/NetDaemonRuntimeTests.cs @@ -10,10 +10,9 @@ namespace NetDaemon.Runtime.Tests.Internal; public class NetDaemonRuntimeTests { [Fact] - public async Task TestExecuteAsync() + public void TestExecuteAsync() { var homeAssistantRunnerMock = new Mock(); - var appModelMock = new Mock(); var serviceProviderMock = new Mock(); var loggerMock = new Mock>(); @@ -25,16 +24,17 @@ public async Task TestExecuteAsync() homeAssistantRunnerMock.Object, new FakeHassSettingsOptions(), new FakeApplicationLocationSettingsOptions(), - appModelMock.Object, serviceProviderMock.Object, loggerMock.Object, Mock.Of() ); var cancelSource = new CancellationTokenSource(5000); - await runtime.ExecuteAsync(cancelSource.Token).ConfigureAwait(false); + var startingTask = runtime.StartAsync(cancelSource.Token); connectSubject.HasObservers.Should().BeTrue(); disconnectSubject.HasObservers.Should().BeTrue(); + + startingTask.IsCompleted.Should().BeFalse(); } [Fact] @@ -68,24 +68,27 @@ public async Task TestOnConnect() serviceCollection.AddTransient>(_ => hassEventSubject); serviceCollection.AddSingleton(_ => homeAssistantRunnerMock.Object); serviceCollection.AddScoped(_ => scopedContext); + serviceCollection.AddSingleton(appModelMock.Object); var serviceProvider = serviceCollection.BuildServiceProvider(); await using var runtime = new NetDaemonRuntime( homeAssistantRunnerMock.Object, new FakeHassSettingsOptions(), new FakeApplicationLocationSettingsOptions(), - appModelMock.Object, serviceProvider, loggerMock.Object, Mock.Of() ); - await runtime.ExecuteAsync(cancelSource.Token).ConfigureAwait(false); + var startingTask = runtime.StartAsync(cancelSource.Token); + startingTask.IsCompleted.Should().BeFalse(); connectSubject.OnNext( homeAssistantConnectionMock.Object ); + await startingTask.ConfigureAwait(false); + - appModelMock.Verify(n => n.InitializeAsync(It.IsAny())); + appModelMock.Verify(n => n.LoadNewApplicationContext(It.IsAny())); } [Fact] @@ -119,32 +122,33 @@ public async Task TestOnDisconnect() serviceCollection.AddTransient>(_ => hassEventSubject); serviceCollection.AddSingleton(_ => homeAssistantRunnerMock.Object); serviceCollection.AddScoped(_ => scopedContext); + serviceCollection.AddSingleton(appModelMock.Object); var serviceProvider = serviceCollection.BuildServiceProvider(); await using var runtime = new NetDaemonRuntime( homeAssistantRunnerMock.Object, new FakeHassSettingsOptions(), new FakeApplicationLocationSettingsOptions(), - appModelMock.Object, serviceProvider, loggerMock.Object, Mock.Of() ); - await runtime.ExecuteAsync(cancelSource.Token).ConfigureAwait(false); + var startingTask = runtime.StartAsync(cancelSource.Token); // First make sure we add an connection connectSubject.OnNext( homeAssistantConnectionMock.Object ); - Assert.NotNull(runtime.InternalConnection); + await startingTask.ConfigureAwait(false); + runtime.IsConnected.Should().BeTrue(); // Then fake a disconnect disconnectSubject.OnNext( DisconnectReason.Client ); - Assert.Null(runtime.InternalConnection); + runtime.IsConnected.Should().BeFalse(); } private class FakeHassSettingsOptions : IOptions diff --git a/src/Runtime/NetDaemon.Runtime/Common/Extensions/HostBuilderExtensions.cs b/src/Runtime/NetDaemon.Runtime/Common/Extensions/HostBuilderExtensions.cs index 3b9f6f437..0895c6bcc 100644 --- a/src/Runtime/NetDaemon.Runtime/Common/Extensions/HostBuilderExtensions.cs +++ b/src/Runtime/NetDaemon.Runtime/Common/Extensions/HostBuilderExtensions.cs @@ -15,17 +15,20 @@ public static IHostBuilder UseNetDaemonAppSettings(this IHostBuilder hostBuilder ) .ConfigureAppConfiguration((ctx, config) => { + // TODO: Most of this seems to be what Host.CreateDefaultBuilder already does config.SetBasePath(Directory.GetCurrentDirectory()); config.AddJsonFile("appsettings.json"); config.AddJsonFile($"appsettings.{ctx.HostingEnvironment.EnvironmentName}.json", true); config.AddEnvironmentVariables(); + var c = config.Build(); var locationSetting = c.GetSection("NetDaemon").Get(); + if (locationSetting?.ApplicationConfigurationFolder is not null) + { + var fullPath = Path.GetFullPath(locationSetting.ApplicationConfigurationFolder); + config.AddYamlAppConfig(fullPath); + } - var fullPath = Path.GetFullPath(locationSetting.ApplicationConfigurationFolder); - - config.AddYamlAppConfig( - fullPath); }); } @@ -33,13 +36,13 @@ public static IHostBuilder UseNetDaemonRuntime(this IHostBuilder hostBuilder) { return hostBuilder .UseAppScopedHaContext() - .ConfigureServices((_, services) => + .ConfigureServices((context, services) => { services.AddLogging(); services.AddHostedService(); services.AddHomeAssistantClient(); - + services.Configure(context.Configuration.GetSection("HomeAssistant")); services.AddSingleton(); }); } -} +} \ No newline at end of file diff --git a/src/Runtime/NetDaemon.Runtime/Common/Extensions/ServiceCollectionExtensions.cs b/src/Runtime/NetDaemon.Runtime/Common/Extensions/ServiceCollectionExtensions.cs index 9753bed2c..fc4bdb6d1 100644 --- a/src/Runtime/NetDaemon.Runtime/Common/Extensions/ServiceCollectionExtensions.cs +++ b/src/Runtime/NetDaemon.Runtime/Common/Extensions/ServiceCollectionExtensions.cs @@ -8,5 +8,6 @@ public static IServiceCollection ConfigureNetDaemonServices(this IServiceCollect { return services.Configure(config.GetSection("NetDaemon")) .Configure(config.GetSection("HomeAssistant")); + // todo: maybe remove 'HomeAssistant' section this here, is this method really needed? If we remove this we can inline the rest } } \ No newline at end of file diff --git a/src/Runtime/NetDaemon.Runtime/Common/IRuntime.cs b/src/Runtime/NetDaemon.Runtime/Common/IRuntime.cs index 644010067..023eb35c9 100644 --- a/src/Runtime/NetDaemon.Runtime/Common/IRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime/Common/IRuntime.cs @@ -2,5 +2,5 @@ namespace NetDaemon.Runtime; public interface IRuntime : IAsyncDisposable { - Task ExecuteAsync(CancellationToken stoppingToken); + Task StartAsync(CancellationToken stoppingToken); } \ No newline at end of file diff --git a/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs b/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs index a9fabf80c..66dfcc1b3 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs @@ -8,7 +8,6 @@ internal class NetDaemonRuntime : IRuntime { private const string Version = "custom_compiled"; private const int TimeoutInSeconds = 30; - private readonly IAppModel _appModel; private readonly ICacheManager _cacheManager; private readonly HomeAssistantSettings _haSettings; @@ -19,13 +18,14 @@ internal class NetDaemonRuntime : IRuntime private readonly IServiceProvider _serviceProvider; private IAppModelContext? _applicationModelContext; private CancellationToken? _stoppingToken; - internal IHomeAssistantConnection? InternalConnection; + private CancellationTokenSource? _runnerCancelationSource; + + public bool IsConnected; public NetDaemonRuntime( IHomeAssistantRunner homeAssistantRunner, IOptions settings, IOptions locationSettings, - IAppModel appModel, IServiceProvider serviceProvider, ILogger logger, ICacheManager cacheManager) @@ -33,7 +33,6 @@ public NetDaemonRuntime( _haSettings = settings.Value; _homeAssistantRunner = homeAssistantRunner; _locationSettings = locationSettings; - _appModel = appModel; _serviceProvider = serviceProvider; _logger = logger; _cacheManager = cacheManager; @@ -43,7 +42,11 @@ public NetDaemonRuntime( internal IReadOnlyCollection ApplicationInstances => _applicationModelContext?.Applications ?? Array.Empty(); - public async Task ExecuteAsync(CancellationToken stoppingToken) + private readonly TaskCompletionSource _startedAndConnected = new(); + + private Task _runnerTask = Task.CompletedTask; + + public async Task StartAsync(CancellationToken stoppingToken) { _logger.LogInformation($"Starting NetDaemon runtime version {Version}."); @@ -57,51 +60,61 @@ public async Task ExecuteAsync(CancellationToken stoppingToken) .Subscribe(); try { - await _homeAssistantRunner.RunAsync( + _runnerCancelationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); + + _runnerTask = _homeAssistantRunner.RunAsync( _haSettings.Host, _haSettings.Port, _haSettings.Ssl, _haSettings.Token, _haSettings.WebsocketPath, TimeSpan.FromSeconds(TimeoutInSeconds), - stoppingToken).ConfigureAwait(false); + _runnerCancelationSource.Token); + + // Make sure we only return after the connection is made and initialization is ready + await _startedAndConnected.Task; } catch (OperationCanceledException) { // Ignore and just stop } - - _logger.LogInformation("Exiting NetDaemon runtime."); } - public async ValueTask DisposeAsync() + private async Task OnHomeAssistantClientConnected( + IHomeAssistantConnection haConnection, + CancellationToken cancelToken) { - await DisposeApplicationsAsync().ConfigureAwait(false); + _logger.LogInformation("Successfully connected to Home Assistant"); + IsConnected = true; + + await _cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false); + + await LoadNewAppContextAsync(haConnection, cancelToken); + + // Now signal that StartAsync may return + _startedAndConnected.SetResult(); } - private async Task OnHomeAssistantClientConnected( - IHomeAssistantConnection haConnection, - CancellationToken cancelToken - ) + private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection, CancellationToken cancelToken) { + var appModel = _serviceProvider.GetService(); + if (appModel == null) return; + try { - InternalConnection = haConnection; - - _logger.LogInformation("Successfully connected to Home Assistant"); + // this logging is a bit weird in this class if (!string.IsNullOrEmpty(_locationSettings.Value.ApplicationConfigurationFolder)) _logger.LogDebug("Loading applications from folder {Path}", Path.GetFullPath(_locationSettings.Value.ApplicationConfigurationFolder)); else _logger.LogDebug("Loading applications with no configuration folder"); - - await _cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false); - - _applicationModelContext = - await _appModel.InitializeAsync(CancellationToken.None).ConfigureAwait(false); + + _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false); // Handle state change for apps if registered - var appStateHandler = _serviceProvider.GetRequiredService(); + var appStateHandler = _serviceProvider.GetService(); + if (appStateHandler == null) return; + await appStateHandler.InitializeAsync(haConnection, _applicationModelContext); } catch (Exception e) @@ -113,7 +126,7 @@ CancellationToken cancelToken private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason) { - if (_stoppingToken?.IsCancellationRequested ?? false) + if (_stoppingToken?.IsCancellationRequested == true || reason == DisconnectReason.Client) { _logger.LogInformation("HassClient disconnected cause of user stopping"); } @@ -131,17 +144,32 @@ private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason) reasonString, TimeoutInSeconds); } - if (InternalConnection is not null) InternalConnection = null; await DisposeApplicationsAsync().ConfigureAwait(false); + IsConnected = false; } private async Task DisposeApplicationsAsync() { if (_applicationModelContext is not null) { - foreach (var applicationInstance in _applicationModelContext.Applications) - await applicationInstance.DisposeAsync().ConfigureAwait(false); + await _applicationModelContext.DisposeAsync(); + _applicationModelContext = null; } } -} + + private bool _isDisposed; + public async ValueTask DisposeAsync() + { + if (_isDisposed) return; + _isDisposed = true; + + await DisposeApplicationsAsync().ConfigureAwait(false); + _runnerCancelationSource?.Cancel(); + try + { + await _runnerTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } +} \ No newline at end of file diff --git a/src/Runtime/NetDaemon.Runtime/Internal/RuntimeService.cs b/src/Runtime/NetDaemon.Runtime/Internal/RuntimeService.cs index d267f6a83..789cc17bf 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/RuntimeService.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/RuntimeService.cs @@ -2,23 +2,31 @@ namespace NetDaemon.Runtime.Internal; internal class RuntimeService : BackgroundService { - private readonly IHostApplicationLifetime _hostLifetime; - private readonly IRuntime _runtime; + private readonly ILogger _logger; - public RuntimeService( - IHostApplicationLifetime hostLifetime, - IRuntime runtime - ) + public RuntimeService(IRuntime runtime, ILogger logger) { - _hostLifetime = hostLifetime; _runtime = runtime; + _logger = logger; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + try + { + await _runtime.StartAsync(cancellationToken); + await base.StartAsync(cancellationToken); + } + catch (OperationCanceledException) { } } + + protected override Task ExecuteAsync(CancellationToken stoppingToken) => Task.CompletedTask; - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + public override async Task StopAsync(CancellationToken cancellationToken) { - await _runtime.ExecuteAsync(stoppingToken).ConfigureAwait(false); - // Stop application if this is exited - _hostLifetime.StopApplication(); + _logger.LogInformation("NetDaemon RuntimeService is stopping"); + await _runtime.DisposeAsync(); + await base.StopAsync(cancellationToken); } } \ No newline at end of file diff --git a/dev/DebugHost/DebugHost.csproj b/src/debug/DebugHost/DebugHost.csproj similarity index 61% rename from dev/DebugHost/DebugHost.csproj rename to src/debug/DebugHost/DebugHost.csproj index 1b6028c4e..45016b7f9 100644 --- a/dev/DebugHost/DebugHost.csproj +++ b/src/debug/DebugHost/DebugHost.csproj @@ -27,17 +27,17 @@ - - - - - - - + + + + + + + - ..\..\.linting\roslynator.ruleset + ..\..\..\.linting\roslynator.ruleset true AllEnabledByDefault diff --git a/dev/DebugHost/Program.cs b/src/debug/DebugHost/Program.cs similarity index 96% rename from dev/DebugHost/Program.cs rename to src/debug/DebugHost/Program.cs index e850a10e1..c038785e7 100644 --- a/dev/DebugHost/Program.cs +++ b/src/debug/DebugHost/Program.cs @@ -13,7 +13,7 @@ try { await Host.CreateDefaultBuilder(args) - .UseNetDaemonAppSettings() + //.UseNetDaemonAppSettings() .UseNetDaemonDefaultLogging() .UseNetDaemonRuntime() .UseNetDaemonTextToSpeech() diff --git a/dev/DebugHost/Properties/launchSettings.json b/src/debug/DebugHost/Properties/launchSettings.json similarity index 100% rename from dev/DebugHost/Properties/launchSettings.json rename to src/debug/DebugHost/Properties/launchSettings.json diff --git a/dev/DebugHost/_appsettings.Development.json b/src/debug/DebugHost/_appsettings.Development.json similarity index 100% rename from dev/DebugHost/_appsettings.Development.json rename to src/debug/DebugHost/_appsettings.Development.json diff --git a/dev/DebugHost/apps/ConcurrencyTestApp.cs b/src/debug/DebugHost/apps/ConcurrencyTestApp.cs similarity index 100% rename from dev/DebugHost/apps/ConcurrencyTestApp.cs rename to src/debug/DebugHost/apps/ConcurrencyTestApp.cs diff --git a/dev/DebugHost/apps/Config/Config.yaml b/src/debug/DebugHost/apps/Config/Config.yaml similarity index 100% rename from dev/DebugHost/apps/Config/Config.yaml rename to src/debug/DebugHost/apps/Config/Config.yaml diff --git a/dev/DebugHost/apps/Config/ConfigApp.cs b/src/debug/DebugHost/apps/Config/ConfigApp.cs similarity index 100% rename from dev/DebugHost/apps/Config/ConfigApp.cs rename to src/debug/DebugHost/apps/Config/ConfigApp.cs diff --git a/dev/DebugHost/apps/Extensions/MqttEntityManagerApp.cs b/src/debug/DebugHost/apps/Extensions/MqttEntityManagerApp.cs similarity index 100% rename from dev/DebugHost/apps/Extensions/MqttEntityManagerApp.cs rename to src/debug/DebugHost/apps/Extensions/MqttEntityManagerApp.cs diff --git a/dev/DebugHost/apps/Extensions/MttEntitySubscriptionApp.cs b/src/debug/DebugHost/apps/Extensions/MttEntitySubscriptionApp.cs similarity index 100% rename from dev/DebugHost/apps/Extensions/MttEntitySubscriptionApp.cs rename to src/debug/DebugHost/apps/Extensions/MttEntitySubscriptionApp.cs diff --git a/src/debug/DebugHost/apps/HelloApp/HelloApp.cs b/src/debug/DebugHost/apps/HelloApp/HelloApp.cs new file mode 100644 index 000000000..7030d2bfb --- /dev/null +++ b/src/debug/DebugHost/apps/HelloApp/HelloApp.cs @@ -0,0 +1,31 @@ +using System; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NetDaemon.AppModel; +using NetDaemon.HassModel; + +namespace Apps; + +[NetDaemonApp] +// [Focus] +public sealed class HelloApp : IAsyncDisposable +{ + private readonly ILogger _logger; + + public HelloApp(IHaContext ha, ILogger logger) + { + _logger = logger; + ha?.Events.Where(n => n.EventType == "test_event").Subscribe( n => + { + logger.LogInformation("Hello testevent"); + }); + // ha?.CallService("notify", "persistent_notification", data: new { message = "Notify me", title = "Hello world!" }); + } + + public ValueTask DisposeAsync() + { + _logger.LogInformation("disposed app"); + return ValueTask.CompletedTask; + } +} \ No newline at end of file diff --git a/dev/DebugHost/apps/YamlApp/YamlApp.cs b/src/debug/DebugHost/apps/YamlApp/YamlApp.cs similarity index 100% rename from dev/DebugHost/apps/YamlApp/YamlApp.cs rename to src/debug/DebugHost/apps/YamlApp/YamlApp.cs diff --git a/dev/DebugHost/apps/YamlApp/YamlApp.yaml b/src/debug/DebugHost/apps/YamlApp/YamlApp.yaml similarity index 100% rename from dev/DebugHost/apps/YamlApp/YamlApp.yaml rename to src/debug/DebugHost/apps/YamlApp/YamlApp.yaml diff --git a/dev/DebugHost/appsettings.json b/src/debug/DebugHost/appsettings.json similarity index 100% rename from dev/DebugHost/appsettings.json rename to src/debug/DebugHost/appsettings.json diff --git a/src/debug/DebugWebHost/DebugWebHost.csproj b/src/debug/DebugWebHost/DebugWebHost.csproj new file mode 100644 index 000000000..3fef87e76 --- /dev/null +++ b/src/debug/DebugWebHost/DebugWebHost.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/src/debug/DebugWebHost/Program.cs b/src/debug/DebugWebHost/Program.cs new file mode 100644 index 000000000..1945e5a39 --- /dev/null +++ b/src/debug/DebugWebHost/Program.cs @@ -0,0 +1,17 @@ +using NetDaemon.HassModel; +using NetDaemon.Runtime; + +var builder = WebApplication.CreateBuilder(args); +builder.Host.UseNetDaemonRuntime(); + +var app = builder.Build(); + +app.MapGet("/", (IHaContext ha) => ha.GetAllEntities().Where(e=>e.EntityId.StartsWith("light")).Select(e => e.EntityId)); +app.MapGet("/Off", (IHaContext ha) => ha.Entity("light.spots_woonkamer_rechts").CallService("turn_off")); +app.MapGet("/On", (IHaContext ha) => ha.Entity("light.spots_woonkamer_rechts").CallService("turn_on")); +app.MapGet("/State/{id}", (IHaContext ha, string id) => ha.Entity(id).EntityState); + +app.MapGet("/Stop", () => app.StopAsync()); + +app.Run(); + diff --git a/src/debug/DebugWebHost/Properties/launchSettings.json b/src/debug/DebugWebHost/Properties/launchSettings.json new file mode 100644 index 000000000..581114cdf --- /dev/null +++ b/src/debug/DebugWebHost/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:59620", + "sslPort": 44389 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5272", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7272;http://localhost:5272", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/debug/DebugWebHost/_appsettings.Development.json b/src/debug/DebugWebHost/_appsettings.Development.json new file mode 100644 index 000000000..10a6b2a87 --- /dev/null +++ b/src/debug/DebugWebHost/_appsettings.Development.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft": "Warning" + }, + "ConsoleThemeType": "System" + }, + "HomeAssistant": { + "Host": "ENTER YOUR IP TO Development Home Assistant here", + "Port": 8124, + "Ssl": false, + "Token": "ENTER YOUR TOKEN", + "InsecureBypassCertificateErrors": false + }, + "NetDaemon": { + "Admin": false, + "ApplicationConfigurationFolder": "./apps" + } +} \ No newline at end of file diff --git a/src/debug/DebugWebHost/appsettings.json b/src/debug/DebugWebHost/appsettings.json new file mode 100644 index 000000000..dde3bbeb3 --- /dev/null +++ b/src/debug/DebugWebHost/appsettings.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "HomeAssistant": { + "Host": "localhost", + "Port": 8123, + "Ssl": false, + "Token": "", + "InsecureBypassCertificateErrors": false + } +}