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
+
+