diff --git a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs index ad30aaa8d..38724e6fb 100644 --- a/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs +++ b/src/AppModel/NetDaemon.AppModel.Tests/AppModelTests.cs @@ -79,7 +79,7 @@ public async Task TestGetApplicationsLocalWithDisabled() Assert.Null((MyAppLocalApp?) application.ApplicationContext?.Instance); // set state to enabled - await application.SetStateAsync(ApplicationState.Enabled).ConfigureAwait(false); + await application.EnableAsync().ConfigureAwait(false); application.State.Should().Be(ApplicationState.Running); Assert.NotNull((MyAppLocalApp?) application.ApplicationContext?.Instance); @@ -118,7 +118,7 @@ public async Task TestGetApplicationsLocalWithEnabled() Assert.NotNull((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance); // set state to enabled - await application.SetStateAsync(ApplicationState.Disabled).ConfigureAwait(false); + await application.DisableAsync().ConfigureAwait(false); application.State.Should().Be(ApplicationState.Disabled); Assert.Null((MyAppLocalAppWithDispose?) application.ApplicationContext?.Instance); } @@ -135,21 +135,6 @@ public async Task TestGetApplicationsWithIdSet() appContext.Should().NotBeEmpty(); } - [Fact] - public async Task TestSetStateToRunningShouldThrowException() - { - // ARRANGE - var provider = Mock.Of(); - var logger = Mock.Of>(); - var factory = Mock.Of(); - - // ACT - var app = new Application(provider, logger, factory); - - // CHECK - await Assert.ThrowsAsync(() => app.SetStateAsync(ApplicationState.Running)); - } - [Fact] public async Task TestGetApplicationsShouldReturnNonErrorOnes() { @@ -181,7 +166,7 @@ public async Task TestGetApplicationsShouldReturnNonErrorOnes() // CHECK loadApps.First(n => n.Id == "LocalAppsWithErrors.MyAppLocalAppWithError") - .State.Should().Be(ApplicationState.Error); + .Enabled.Should().BeTrue(); // Verify that the error is logged loggerMock.Verify( @@ -222,10 +207,13 @@ public async Task TestGetApplicationsLocalWithAsyncDisposable() // check the application instance is init ok var application = (Application) loadApps.First(n => n.Id == "LocalApps.MyAppLocalAppWithAsyncDispose"); var app = (MyAppLocalAppWithAsyncDispose?) application.ApplicationContext?.Instance; - application.State.Should().Be(ApplicationState.Running); + application.IsRunning.Should().BeTrue(); + application.Enabled.Should().BeTrue(); await application.DisposeAsync().ConfigureAwait(false); app!.AsyncDisposeIsCalled.Should().BeTrue(); app.DisposeIsCalled.Should().BeFalse(); + application.IsRunning.Should().BeFalse(); + application.Enabled.Should().BeTrue(); } [Fact] diff --git a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs index f0d0d27d1..50313427b 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/Extensions/ServiceCollectionExtension.cs @@ -121,8 +121,9 @@ private static IServiceCollection AddAppModelIfNotExist(this IServiceCollection services .AddSingleton() .AddSingleton(s => s.GetRequiredService()) - .AddTransient() - .AddTransient(s => s.GetRequiredService()) + + // IAppModelContext is resolved via AppModelImpl which it itself a Singleton, so it will return the current AppModelContext + .AddTransient(s => s.GetRequiredService().CurrentAppModelContext ?? throw new InvalidOperationException("No AppModelContext is currently loaded")) .AddTransient() .AddScopedConfigurationBinder() .AddScopedAppServices() diff --git a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs index 334a87cdb..ed75e159a 100644 --- a/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs +++ b/src/AppModel/NetDaemon.AppModel/Common/IApplication.cs @@ -11,13 +11,23 @@ public interface IApplication : IAsyncDisposable string? Id { get; } /// - /// Current state of the application + /// Indicates if this application is currently Enabled /// - ApplicationState State { get; } + public bool Enabled { get; } /// - /// Sets state for application + /// Enables the App and loads if possible /// - /// The state to set - Task SetStateAsync(ApplicationState state); + public Task EnableAsync(); + + /// + /// Disable the app and unload (Dispose) it if it is running + /// + public Task DisableAsync(); + + /// + /// The currently running instance of the app (if any) + /// + public object? Instance { get; } + } \ No newline at end of file diff --git a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs index 7a5433b9d..2b79141ee 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModel.cs @@ -12,11 +12,15 @@ public AppModelImpl(IServiceProvider provider) _provider = provider; } + public IAppModelContext? CurrentAppModelContext { get; private set; } + public async Task LoadNewApplicationContext(CancellationToken cancellationToken) { - // Create a new AppModelContext - var appModelContext = _provider.GetRequiredService(); + var appModelContext = ActivatorUtilities.CreateInstance(_provider); await appModelContext.InitializeAsync(cancellationToken); + + // Assign to CurrentAppModelContext so it can be resolved via DI + CurrentAppModelContext = appModelContext; 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 6bf0dc8d8..5e786e68f 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/AppModelContext.cs @@ -4,35 +4,30 @@ namespace NetDaemon.AppModel.Internal; internal class AppModelContext : IAppModelContext { - private readonly List _applications = new(); + private readonly List _applications; - private readonly IEnumerable _appFactoryProviders; - private readonly IServiceProvider _provider; - private readonly FocusFilter _focusFilter; - private ILogger _logger; + private readonly ILogger _logger; private bool _isDisposed; public AppModelContext(IEnumerable appFactoryProviders, IServiceProvider provider, FocusFilter focusFilter, ILogger logger) { - _appFactoryProviders = appFactoryProviders; - _provider = provider; - _focusFilter = focusFilter; _logger = logger; + + var factories = appFactoryProviders.SelectMany(p => p.GetAppFactories()).ToList(); + + var filteredFactories = focusFilter.FilterFocusApps(factories); + + _applications = filteredFactories.Select(factory => ActivatorUtilities.CreateInstance(provider, factory)) + .ToList(); } - public IReadOnlyCollection Applications => _applications; + public IReadOnlyCollection Applications => _applications.AsReadOnly(); public async Task InitializeAsync(CancellationToken cancellationToken) { - var factories = _appFactoryProviders.SelectMany(provider => provider.GetAppFactories()).ToList(); - - var filteredFactories = _focusFilter.FilterFocusApps(factories); - - foreach (var factory in filteredFactories) + foreach (var application in _applications) { - var app = ActivatorUtilities.CreateInstance(_provider, factory); - await app.InitializeAsync().ConfigureAwait(false); - _applications.Add(app); + await application.InitializeAsync(); } _logger.LogInformation("Finished loading applications: {state}", diff --git a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs index 25b96bc30..4e6009739 100644 --- a/src/AppModel/NetDaemon.AppModel/Internal/Application.cs +++ b/src/AppModel/NetDaemon.AppModel/Internal/Application.cs @@ -26,72 +26,48 @@ public Application(IServiceProvider provider, ILogger logger, IAppF // Used in tests internal ApplicationContext? ApplicationContext { get; private set; } - public string Id => _appFactory.Id; - - public ApplicationState State - { - get - { - if (_isErrorState) - return ApplicationState.Error; + public bool Enabled { get; private set; } - return ApplicationContext is null ? ApplicationState.Disabled : ApplicationState.Running; - } - } + public bool IsRunning { get; private set; } - public async Task SetStateAsync(ApplicationState state) - { - switch (state) - { - case ApplicationState.Enabled: - await LoadApplication(state); - break; - case ApplicationState.Disabled: - await UnloadApplication(state); - break; - case ApplicationState.Error: - _isErrorState = true; - await SaveStateIfStateManagerExistAsync(state); - break; - case ApplicationState.Running: - throw new ArgumentException("Running state can only be set internally", nameof(state)); - } - } + // TODO: see if we can remove this and refactor the possible states + public ApplicationState State => + _isErrorState ? ApplicationState.Error : + IsRunning ? ApplicationState.Running : + Enabled ? ApplicationState.Enabled : + ApplicationState.Disabled; + + public string Id => _appFactory.Id; - public async ValueTask DisposeAsync() - { - if (ApplicationContext is not null) await ApplicationContext.DisposeAsync().ConfigureAwait(false); - } + public object? Instance => ApplicationContext?.Instance; - private async Task UnloadApplication(ApplicationState state) + + public async Task InitializeAsync() { - if (ApplicationContext is not null) + if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false)) { - await ApplicationContext.DisposeAsync().ConfigureAwait(false); - ApplicationContext = null; - _logger.LogInformation("Successfully unloaded app {Id}", Id); - await SaveStateIfStateManagerExistAsync(state).ConfigureAwait(false); + Enabled = true; + await LoadAsync().ConfigureAwait(false); } } - private async Task LoadApplication(ApplicationState state) + private async Task ShouldInstanceApplicationAsync(string id) { - // first we save state "Enabled", this will also - // end up being state "Running" if instancing is successful - // or "Error" if instancing the app fails - await SaveStateIfStateManagerExistAsync(state); - if (ApplicationContext is null) - await InstanceApplication().ConfigureAwait(false); + if (_appStateManager is null) + return true; + return await _appStateManager.GetStateAsync(id).ConfigureAwait(false) == ApplicationState.Enabled; } - public async Task InitializeAsync() + public async Task EnableAsync() { - if (await ShouldInstanceApplicationAsync(Id).ConfigureAwait(false)) - await InstanceApplication().ConfigureAwait(false); + await SaveStateIfStateManagerExistAsync(ApplicationState.Enabled); + await LoadAsync(); } - - private async Task InstanceApplication() + + public async Task LoadAsync() { + if (ApplicationContext is not null) return; + try { ApplicationContext = new ApplicationContext(_provider, _appFactory); @@ -106,28 +82,47 @@ private async Task InstanceApplication() Id); await initAsyncTask; // Continue to wait even if timeout is set so we do not miss errors + IsRunning = true; + _isErrorState = false; - await SaveStateIfStateManagerExistAsync(ApplicationState.Running); _logger.LogInformation("Successfully loaded app {Id}", Id); } catch (Exception e) { + _isErrorState = true; _logger.LogError(e, "Error loading app {Id}", Id); - await SetStateAsync(ApplicationState.Error); } } + + public async Task DisableAsync() + { + await UnloadAsync(); + Enabled = false; + + await SaveStateIfStateManagerExistAsync(ApplicationState.Disabled).ConfigureAwait(false); + } - private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState) + public async Task UnloadAsync() { - if (_appStateManager is not null) - await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false); + if (ApplicationContext is not null) + { + await ApplicationContext.DisposeAsync().ConfigureAwait(false); + ApplicationContext = null; + _logger.LogInformation("Successfully unloaded app {Id}", Id); + } + + IsRunning = false; + } + + + public async ValueTask DisposeAsync() + { + await UnloadAsync(); } - private async Task ShouldInstanceApplicationAsync(string id) + private async Task SaveStateIfStateManagerExistAsync(ApplicationState appState) { - if (_appStateManager is null) - return true; - return await _appStateManager.GetStateAsync(id).ConfigureAwait(false) - == ApplicationState.Enabled; + if (_appStateManager is not null) + await _appStateManager.SaveStateAsync(Id, appState).ConfigureAwait(false); } } diff --git a/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj b/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj index 3fabace74..ab7b8771c 100644 --- a/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj +++ b/src/AppModel/NetDaemon.AppModel/NetDaemon.AppModel.csproj @@ -30,7 +30,7 @@ - + diff --git a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs index 6f206e3ac..90bb550d5 100644 --- a/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs +++ b/src/Runtime/NetDaemon.Runtime.Tests/Internal/AppStateManagerTests.cs @@ -335,7 +335,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode }.ToJsonElement() }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Once); + appMock.Verify(n => n.DisableAsync(), Times.Once); } [Fact] @@ -390,7 +390,7 @@ await homeAssistantStateUpdater.InitializeAsync(haConnectionMock.Object, appMode }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never); + appMock.Verify(n => n.DisableAsync(), Times.Never); } [Fact] @@ -441,7 +441,7 @@ public async Task TestAppOneStateIsNullShouldNotCallSetStateAsync() }); // ASSERT - appMock.Verify(n => n.SetStateAsync(ApplicationState.Disabled), Times.Never); + appMock.Verify(n => n.DisableAsync(), Times.Never); } private (Mock connection, IServiceProvider serviceProvider) SetupProviderAndMocks() diff --git a/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs b/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs index b9c09c583..fc98f336d 100644 --- a/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs +++ b/src/Runtime/NetDaemon.Runtime/Internal/AppStateManager.cs @@ -42,6 +42,7 @@ await _appStateRepository.RemoveNotUsedStatesAsync(appContext.Applications.Selec if (changedEvent.NewState.State == changedEvent.OldState.State) // We only care about changed state return; + foreach (var app in appContext.Applications) { var entityId = @@ -49,13 +50,14 @@ await _appStateRepository.RemoveNotUsedStatesAsync(appContext.Applications.Selec throw new InvalidOperationException(), _hostEnvironment.IsDevelopment()); if (entityId != changedEvent.NewState.EntityId) continue; - var appState = changedEvent.NewState?.State == "on" - ? ApplicationState.Enabled - : ApplicationState.Disabled; - - await app.SetStateAsync( - appState - ); + if (changedEvent.NewState?.State == "on") + { + await app.EnableAsync().ConfigureAwait(false); + } + else + { + await app.DisableAsync().ConfigureAwait(false); + } break; } }).Subscribe(); diff --git a/src/debug/DebugHost/DebugHost.csproj b/src/debug/DebugHost/DebugHost.csproj index 78e1e1a0f..595d11168 100644 --- a/src/debug/DebugHost/DebugHost.csproj +++ b/src/debug/DebugHost/DebugHost.csproj @@ -30,6 +30,7 @@ + diff --git a/src/debug/DebugHost/Program.cs b/src/debug/DebugHost/Program.cs index c038785e7..e153ceee5 100644 --- a/src/debug/DebugHost/Program.cs +++ b/src/debug/DebugHost/Program.cs @@ -7,6 +7,7 @@ using NetDaemon.Extensions.Logging; using NetDaemon.Extensions.Tts; using NetDaemon.Extensions.MqttEntityManager; +using NetDaemon.Extensions.Scheduler; #pragma warning disable CA1812 @@ -23,6 +24,7 @@ await Host.CreateDefaultBuilder(args) // change type of compilation here // .AddAppsFromSource(true) .AddAppsFromAssembly(Assembly.GetEntryAssembly()!) + .AddNetDaemonScheduler() // Remove this is you are not running the integration! .AddNetDaemonStateManager() ) diff --git a/src/debug/DebugHost/apps/Client/ClientDebug.cs b/src/debug/DebugHost/apps/Client/ClientDebug.cs index f5e3bfa21..a321c9f87 100644 --- a/src/debug/DebugHost/apps/Client/ClientDebug.cs +++ b/src/debug/DebugHost/apps/Client/ClientDebug.cs @@ -1,53 +1,41 @@ using System; +using System.Linq; +using System.Reactive.Concurrency; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using NetDaemon.AppModel; -using NetDaemon.HassModel; namespace Apps; [NetDaemonApp] -// [Focus] -public sealed class ClientApp : IAsyncDisposable +[Focus] +public sealed class ClientApp { private readonly ILogger _logger; - public ClientApp(ILogger logger, ITriggerManager triggerManager) + public ClientApp(IAppModelContext ctx, IScheduler scheduler) { - _logger = logger; - - var triggerObservable = triggerManager.RegisterTrigger( - new - { - platform = "state", - entity_id = new string[] { "media_player.vardagsrum" }, - from = new string[] { "idle", "playing" }, - to = "off" - }); - - triggerObservable.Subscribe(n => - _logger.LogCritical("Got trigger message: {Message}", n) - ); - - var timePatternTriggerObservable = triggerManager.RegisterTrigger(new - { - platform = "time_pattern", - id = "some id", - seconds = "/1" - }); - - var disposedSubscription = timePatternTriggerObservable.Subscribe(n => - _logger.LogCritical("Got trigger message: {Message}", n) - ); + var apps = ctx.Applications.ToList(); + scheduler.Schedule(TimeSpan.FromSeconds(2), () => apps[1].DisableAsync().Wait()); + } +} + +[NetDaemonApp] +[Focus] +public sealed class ClientApp2 : IAsyncDisposable +{ + private readonly ILogger _logger; + + public ClientApp2(IAppModelContext ctx) + { + var apps = ctx.Applications.ToList(); } public ValueTask DisposeAsync() { - _logger.LogInformation("disposed app"); + _logger.LogInformation("disposed"); return ValueTask.CompletedTask; } - - record TimePatternResult(string id, string alias, string platform, DateTimeOffset now, string description); }