From 29b0eb4ac3df25b64b0d92edf5bd279461e01720 Mon Sep 17 00:00:00 2001 From: helto4real Date: Fri, 25 Dec 2020 12:54:12 +0100 Subject: [PATCH] Cleaner disconnect/reconnect wou multiple instance --- .devcontainer/devcontainer.json | 20 ++- DEV.md | 9 +- .../NetDaemon.App/Common/NetDaemonAppBase.cs | 10 +- .../Common/Reactive/AppDaemonRxApp.cs | 7 +- .../Common/Reactive/ObservableBase.cs | 1 + src/App/NetDaemon.App/NetDaemon.App.csproj | 2 +- .../NetDaemon.Daemon/Daemon/NetDaemonHost.cs | 42 +++-- .../NetDaemon.Daemon/NetDaemon.Daemon.csproj | 2 +- .../DaemonRunner/DaemonRunner.csproj | 2 +- .../Service/App/LocalDaemonAppCompiler.cs | 3 +- .../DaemonRunner/Service/RunnerService.cs | 167 ++++++++++-------- src/DevelopmentApps/apps/DebugApp/DebugApp.cs | 12 +- src/Service/.gitignore | 1 + src/Service/_appsettings.Development.json | 18 +- tests/Docker/HA/docker-compose.yaml | 13 ++ tests/Docker/README.md | 7 + 16 files changed, 190 insertions(+), 126 deletions(-) create mode 100644 src/Service/.gitignore create mode 100644 tests/Docker/HA/docker-compose.yaml create mode 100644 tests/Docker/README.md diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4fcfeb4e7..0abc640c2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,15 +16,17 @@ "coverage-gutters.showGutterCoverage": false, "coverage-gutters.showLineCoverage": true, }, - "remoteEnv": { - "HOMEASSISTANT__TOKEN": "${localEnv:HOMEASSISTANT__TOKEN}", - "HOMEASSISTANT__HOST": "${localEnv:HOMEASSISTANT__HOST}", - "HOMEASSISTANT__PORT": "${localEnv:HOMEASSISTANT__PORT}", - "LOGGING__MINIMUMLEVEL": "${localEnv:LOGGING__MINIMUMLEVEL}", - "NETDAEMON__GENERATEENTITIES": "${localEnv:NETDAEMON__GENERATEENTITIES}", - "NETDAEMON__ADMIN": "${localEnv:NETDAEMON__ADMIN}", - "ASPNETCORE_URLS": "${localEnv:ASPNETCORE_URLS}" - }, + // You can uncomment these if you rather have settings in + // Host environment varialbes. + // "remoteEnv": { + // "HOMEASSISTANT__TOKEN": "${localEnv:HOMEASSISTANT__TOKEN}", + // "HOMEASSISTANT__HOST": "${localEnv:HOMEASSISTANT__HOST}", + // "HOMEASSISTANT__PORT": "${localEnv:HOMEASSISTANT__PORT}", + // "LOGGING__MINIMUMLEVEL": "${localEnv:LOGGING__MINIMUMLEVEL}", + // "NETDAEMON__GENERATEENTITIES": "${localEnv:NETDAEMON__GENERATEENTITIES}", + // "NETDAEMON__ADMIN": "${localEnv:NETDAEMON__ADMIN}", + // "ASPNETCORE_URLS": "${localEnv:ASPNETCORE_URLS}" + // }, // "postCreateCommand": "dotnet restore && .devcontainer/install_prettyprompt.sh", // Uncomment the next line if you want to publish or forward any ports. "forwardPorts": [ diff --git a/DEV.md b/DEV.md index e9573a62e..5c5b9d8e1 100644 --- a/DEV.md +++ b/DEV.md @@ -1,8 +1,13 @@ # Developing NetDaemon These instructions are for developing NetDaemon. For apps please use [the docs](https://netdaemon.xyz). +For your convenience we provided with a docker setup with Home Assistant you can run on your development machine. +`tests/Docker/HA` you will find the docker-compose file. Run it outside the devcontainer. Remarkts that you should use port 8124 connecting to this instance of Home Assistant. + +## Use appsettings.Development.json +Copy the "_appsettings.Development.json under `src/Service` ## Setup the environment vars -Easiest is to setup environment varables for your Home Assistant instance +Alternative to using appsettings for development is to use environment varables for your Home Assistant instance | Environment variable | Description | | ------ | ------ | @@ -12,6 +17,6 @@ Easiest is to setup environment varables for your Home Assistant instance | NETDAEMON__GENERATEENTITIES | Generate entities, recommed set false unless debugging | | NETDAEMON__APPSOURCE | The folder/project/dll where it will find daemon. Set this to empty `""` to debug apps local. If needed to debug the dynamic source compilation, set to `/workspaces/netdaemon/Service/apps` | -Use `src/Service/apps` as starting point to debug your stuff! +Use `src/Development/apps` as starting point to debug your stuff! Good luck diff --git a/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs b/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs index 87d879692..9884e8634 100644 --- a/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs +++ b/src/App/NetDaemon.App/Common/NetDaemonAppBase.cs @@ -163,27 +163,27 @@ public async Task RestoreAppStateAsync() if (isDisabled) { IsEnabled = false; - if (appState != "off") + if (appState == "on") { dynamic serviceData = new FluentExpandoObject(); serviceData.entity_id = EntityId; + await _daemon.SetStateAsync(EntityId, "off").ConfigureAwait(false); await _daemon.CallServiceAsync("switch", "turn_off", serviceData); } return; } - else if (appState == null || (appState != "on" && appState != "off")) + else { IsEnabled = true; - if (appState != "on") + if (appState == "off") { dynamic serviceData = new FluentExpandoObject(); serviceData.entity_id = EntityId; + await _daemon.SetStateAsync(EntityId, "on").ConfigureAwait(false); await _daemon.CallServiceAsync("switch", "turn_on", serviceData); } - return; } - IsEnabled = appState == "on"; } /// diff --git a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs index 4a783b56b..24537d81b 100644 --- a/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs +++ b/src/App/NetDaemon.App/Common/Reactive/AppDaemonRxApp.cs @@ -295,10 +295,13 @@ internal virtual IDisposable CreateObservableIntervall(TimeSpan timespan, Action } catch (Exception e) { - LogError(e, "Error, RunEvery APP: {app}", Id ?? "unknown"); + LogError(e, "Error, ObservableIntervall APP: {app}", Id ?? "unknown"); } }, - () => LogTrace("Exiting RunEvery for app {app}, {trigger}:{span}", Id!, timespan) + ex => + { + LogTrace("Exiting ObservableIntervall for app {app}, {trigger}:{span}", Id!, timespan); + } , result.Token); return result; diff --git a/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs b/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs index 214423161..fec144b97 100644 --- a/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs +++ b/src/App/NetDaemon.App/Common/Reactive/ObservableBase.cs @@ -84,6 +84,7 @@ public void Dispose() if (_observer is not null) { _observers.TryRemove(_observer, out _); + _observer.OnCompleted(); } // System.Console.WriteLine($"Subscribers:{_observers.Count}"); } diff --git a/src/App/NetDaemon.App/NetDaemon.App.csproj b/src/App/NetDaemon.App/NetDaemon.App.csproj index c2c416c7a..72f6946b4 100644 --- a/src/App/NetDaemon.App/NetDaemon.App.csproj +++ b/src/App/NetDaemon.App/NetDaemon.App.csproj @@ -22,7 +22,7 @@ - + diff --git a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs index 757adc079..bdbd9a5fa 100644 --- a/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs +++ b/src/Daemon/NetDaemon.Daemon/Daemon/NetDaemonHost.cs @@ -461,6 +461,17 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat HassEvent changedEvent = await _hassClient.ReadEventAsync(cancellationToken).ConfigureAwait(false); if (changedEvent != null) { + if (changedEvent.Data is HassServiceEventData hseData) + { + if (hseData.Domain == "homeassistant" && + (hseData.Service == "stop" || hseData.Service == "restart")) + { + // The user stopped HA so just stop processing messages + Logger.LogInformation("User {action} Home Assistant, will try to reconnect...", + hseData.Service == "stop" ? "stopping" : "restarting"); + return; + } + } // Remove all completed Tasks _eventHandlerTasks.RemoveAll(x => x.IsCompleted); _eventHandlerTasks.Add(HandleNewEvent(changedEvent, cancellationToken)); @@ -474,13 +485,20 @@ public async Task Run(string host, short port, bool ssl, string token, Cancellat } catch (OperationCanceledException) { - // Normal + // Normal operation, ignore and return } catch (Exception e) { Connected = false; Logger.LogError(e, "Error, during operation"); } + finally + { + // Set cancel token to avoid background processes + // to access disconnected Home Assistant + Connected = false; + _cancelTokenSource.Cancel(); + } } public IScript RunScript(INetDaemonApp app, params string[] entityId) @@ -584,44 +602,37 @@ public async Task Stop() { try { - if (_stopped) - { - return; - } - Logger.LogDebug("Try stopping Instance NetDaemonHost"); + Logger.LogTrace("Try stopping Instance NetDaemonHost"); await UnloadAllApps().ConfigureAwait(false); await _scheduler.Stop().ConfigureAwait(false); - await _hassClient.CloseAsync().ConfigureAwait(false); InternalState.Clear(); _stopped = true; Connected = false; - - Logger.LogInformation("Stopped Instance NetDaemonHost"); + await _hassClient.CloseAsync().ConfigureAwait(false); + Logger.LogTrace("Stopped Instance NetDaemonHost"); } catch (Exception e) { - Logger.LogError(e, "Error stopping NetDaemon"); + Logger.LogError("Error stopping NetDaemon, use trace level for details"); + Logger.LogTrace(e, "Error stopping NetDaemon"); } } /// public async Task UnloadAllApps() { - if (_runningAppInstances is null || _runningAppInstances.Count() == 0) - return; - foreach (var app in _allAppInstances) { await app.Value.DisposeAsync().ConfigureAwait(false); } - _runningAppInstances.Clear(); _allAppInstances.Clear(); + _runningAppInstances.Clear(); } /// @@ -1228,11 +1239,12 @@ private async Task LoadAllApps() foreach (INetDaemonAppBase appInstance in instancedApps!) { + _allAppInstances[appInstance.Id!] = appInstance; if (await RestoreAppState(appInstance).ConfigureAwait(false)) { _runningAppInstances[appInstance.Id!] = appInstance; } - _allAppInstances[appInstance.Id!] = appInstance; + } // Now run initialize on all sorted by dependencies diff --git a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj index a06ba61d2..ee532db4a 100644 --- a/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj +++ b/src/Daemon/NetDaemon.Daemon/NetDaemon.Daemon.csproj @@ -20,7 +20,7 @@ Home Assistant - + diff --git a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj index 24c63b835..24dff151e 100644 --- a/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj +++ b/src/DaemonRunner/DaemonRunner/DaemonRunner.csproj @@ -21,7 +21,7 @@ - + diff --git a/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs b/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs index 87a5e8a68..ddd6027ad 100644 --- a/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs +++ b/src/DaemonRunner/DaemonRunner/Service/App/LocalDaemonAppCompiler.cs @@ -22,7 +22,7 @@ public LocalDaemonAppCompiler(ILogger logger) public IEnumerable GetApps() { _logger.LogDebug("Loading local assembly apps..."); - + var assemblies = LoadAll(); var apps = assemblies.SelectMany(x => x.GetTypesWhereSubclassOf()).ToList(); @@ -48,6 +48,7 @@ private IEnumerable LoadAll() foreach (var netDaemonDllToLoadDynamically in netDaemonDllsToLoadDynamically) { + _logger.LogTrace("Loading {dll} into AssemblyLoadContext", netDaemonDllToLoadDynamically); AssemblyLoadContext.Default.LoadFromAssemblyPath(netDaemonDllToLoadDynamically); } diff --git a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs index 61b6c5faa..eeb983834 100644 --- a/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs +++ b/src/DaemonRunner/DaemonRunner/Service/RunnerService.cs @@ -37,6 +37,8 @@ public class RunnerService : BackgroundService private string? _sourcePath = null; + private bool _hasConnectedBefore = false; + public RunnerService( ILoggerFactory loggerFactory, IOptions netDaemonSettings, @@ -81,115 +83,124 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) if (_sourcePath is string && !Directory.Exists(_sourcePath)) throw new FileNotFoundException("Source path {path} does not exist", _sourcePath); - var hasConnectedBefore = false; + _loadedDaemonApps = null; await using var daemonHost = _serviceProvider.GetService() ?? throw new ApplicationException("Failed to get service for NetDaemonHost"); + { + await Run(daemonHost, stoppingToken); + } + } + catch (OperationCanceledException) + { + } // Normal exit + catch (Exception e) + { + _logger.LogError(e, "NetDaemon had unhandled exception, closing..."); + } - while (!stoppingToken.IsCancellationRequested) + _logger.LogInformation("NetDaemon exited!"); + } + + private async Task Run(NetDaemonHost daemonHost, CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + try { - try + if (_hasConnectedBefore) { - if (hasConnectedBefore) - { - // This is due to re-connect, it must be a re-connect - // so delay before retry connect again - await Task.Delay(ReconnectInterval, stoppingToken).ConfigureAwait(false); // Wait x seconds - _logger.LogInformation($"Restarting NeDaemon (version {Version})..."); - } + // This is due to re-connect, it must be a re-connect + // so delay before retry connect again + await Task.Delay(ReconnectInterval, stoppingToken).ConfigureAwait(false); // Wait x seconds + _logger.LogInformation($"Restarting NeDaemon (version {Version})..."); + } - var daemonHostTask = daemonHost.Run( - _homeAssistantSettings.Host, - _homeAssistantSettings.Port, - _homeAssistantSettings.Ssl, - _homeAssistantSettings.Token, - stoppingToken - ); + var daemonHostTask = daemonHost.Run( + _homeAssistantSettings.Host, + _homeAssistantSettings.Port, + _homeAssistantSettings.Ssl, + _homeAssistantSettings.Token, + stoppingToken + ); - if (await WaitForDaemonToConnect(daemonHost, stoppingToken).ConfigureAwait(false) == false) - { - continue; - } + if (await WaitForDaemonToConnect(daemonHost, stoppingToken).ConfigureAwait(false) == false) + { + continue; + } - if (!stoppingToken.IsCancellationRequested) + if (!stoppingToken.IsCancellationRequested) + { + if (daemonHost.Connected) { - if (daemonHost.Connected) + try { - try - { - // Generate code if requested - if (_sourcePath is string) - await GenerateEntities(daemonHost, _sourcePath); - - if (_loadedDaemonApps is null) - _loadedDaemonApps = _daemonAppCompiler.GetApps(); - - if (_loadedDaemonApps is null || !_loadedDaemonApps.Any()) - { - _logger.LogError("No apps found, exiting..."); - return; - } - - IInstanceDaemonApp? codeManager = new CodeManager(_loadedDaemonApps, _logger, _yamlConfig); - await daemonHost.Initialize(codeManager).ConfigureAwait(false); - - // Wait until daemon stops - await daemonHostTask.ConfigureAwait(false); - if (!stoppingToken.IsCancellationRequested) - { - // It is disconnected, wait - // _logger.LogWarning($"Home assistant is unavailable, retrying in {ReconnectInterval / 1000} seconds..."); - } - } - catch (TaskCanceledException) - { - _logger.LogInformation("Canceling NetDaemon service..."); - } - catch (Exception e) + // Generate code if requested + if (_sourcePath is string) + await GenerateEntities(daemonHost, _sourcePath); + + if (_loadedDaemonApps is null) + _loadedDaemonApps = _daemonAppCompiler.GetApps(); + + if (_loadedDaemonApps is null || !_loadedDaemonApps.Any()) { - _logger.LogError(e, "Failed to load applications"); + _logger.LogError("No apps found, exiting..."); + return; } + + IInstanceDaemonApp? codeManager = new CodeManager(_loadedDaemonApps, _logger, _yamlConfig); + await daemonHost.Initialize(codeManager).ConfigureAwait(false); + + // Wait until daemon stops + await daemonHostTask.ConfigureAwait(false); + } - else + catch (TaskCanceledException) { - _logger.LogWarning($"Home Assistant Core still unavailable, retrying in {ReconnectInterval / 1000} seconds..."); + _logger.LogInformation("Canceling NetDaemon service..."); + } + catch (Exception e) + { + _logger.LogError(e, "Failed to load applications"); } } - } - catch (OperationCanceledException) - { - if (!stoppingToken.IsCancellationRequested) + else { - _logger.LogWarning($"Home assistant is disconnected, retrying in {ReconnectInterval / 1000} seconds..."); + _logger.LogWarning($"Home Assistant Core still unavailable, retrying in {ReconnectInterval / 1000} seconds..."); } } - catch (Exception e) + } + catch (OperationCanceledException) + { + if (!stoppingToken.IsCancellationRequested) { - _logger.LogError(e, "MAJOR ERROR!"); + _logger.LogWarning($"Home assistant is disconnected, retrying in {ReconnectInterval / 1000} seconds..."); } - finally + } + catch (Exception e) + { + _logger.LogError(e, "MAJOR ERROR!"); + } + finally + { + try { await daemonHost.Stop().ConfigureAwait(false); } + catch (Exception e) + { + _logger.LogError("Error stopping NetDaemonInstance, enable trace level logging for details"); + _logger.LogTrace(e, "Error stopping NetDaemonInstance"); + } + } - // If we reached here it could be a re-connect - hasConnectedBefore = true; + // If we reached here it could be a re-connect + _hasConnectedBefore = true; - } } - catch (OperationCanceledException) - { - } // Normal exit - catch (Exception e) - { - _logger.LogError(e, "NetDaemon had unhandled exception, closing..."); - } - - _logger.LogInformation("NetDaemon exited!"); } - private async Task GenerateEntities(NetDaemonHost daemonHost, string sourceFolder) { if (!_netDaemonSettings.GenerateEntities.GetValueOrDefault()) diff --git a/src/DevelopmentApps/apps/DebugApp/DebugApp.cs b/src/DevelopmentApps/apps/DebugApp/DebugApp.cs index 434042c12..c0bcccb35 100644 --- a/src/DevelopmentApps/apps/DebugApp/DebugApp.cs +++ b/src/DevelopmentApps/apps/DebugApp/DebugApp.cs @@ -5,13 +5,21 @@ namespace NetDaemon.DevelopmentApps.apps.DebugApp { - /// Use this class as startingpoint for debugging + /// Use this class as startingpoint for debugging + /// public class DebugApp : NetDaemonRxApp { + // Use two guids, one when instanced and one when initialized + // can track errors with instancing + Guid _instanceId = Guid.NewGuid(); + public DebugApp() : base() + { + } public override void Initialize() { - RunEvery(TimeSpan.FromSeconds(5), () => Log("Hello developer!")); + var uid = Guid.NewGuid(); + RunEvery(TimeSpan.FromSeconds(5), () => Log("Hello developer! from instance {instanceId} - {id}", _instanceId, uid)); } [HomeAssistantServiceCall] diff --git a/src/Service/.gitignore b/src/Service/.gitignore new file mode 100644 index 000000000..1203719ae --- /dev/null +++ b/src/Service/.gitignore @@ -0,0 +1 @@ +.storage \ No newline at end of file diff --git a/src/Service/_appsettings.Development.json b/src/Service/_appsettings.Development.json index 03d2f3b3d..3a41a8d10 100644 --- a/src/Service/_appsettings.Development.json +++ b/src/Service/_appsettings.Development.json @@ -1,17 +1,17 @@ { "Logging": { + "MinimumLevel": "Debug", "ConsoleThemeType": "System" }, - "NetDaemon": { - "Admin": false, - "ProjectFolder": "", - "SourceFolder": "", - "GenerateEntities": false - }, "HomeAssistant": { - "Host": "HOME_ASSISTANT_IP", - "Port": 8123, + "Host": "ENTER YOUR IP TO Development Home Assistant here", + "Port": 8124, "Ssl": false, - "Token": "TOKEN" + "Token": "ENTER YOUR TOKEN" + }, + "NetDaemon": { + "Admin": false, + "GenerateEntities": false, + "AppSource": "./" } } \ No newline at end of file diff --git a/tests/Docker/HA/docker-compose.yaml b/tests/Docker/HA/docker-compose.yaml new file mode 100644 index 000000000..301d74860 --- /dev/null +++ b/tests/Docker/HA/docker-compose.yaml @@ -0,0 +1,13 @@ +version: '3' +services: + homeassistant: + container_name: home-assistant + image: homeassistant/home-assistant:stable + volumes: + - ./config:/config + environment: + - TZ=Europe/Stockholm + restart: on-failure + # Map the port to 8124 instead of 8123 not to compete with any local Home Assistant + ports: + - "8124:8123" \ No newline at end of file diff --git a/tests/Docker/README.md b/tests/Docker/README.md new file mode 100644 index 000000000..cf98438d4 --- /dev/null +++ b/tests/Docker/README.md @@ -0,0 +1,7 @@ +# Docker test images +This is the setup for testing Home Assistant in docker. If you are using devcontainer these have to be run outside the devcontainer. + +## HA/docker-compose.yaml +This is just a normal Home Assistant instance with some standard configurations for easy testing + +