• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

net-daemon / netdaemon / 28740498188

05 Jul 2026 12:17PM UTC coverage: 83.418% (-0.6%) from 83.983%
28740498188

Pull #1392

github

web-flow
Merge 4c3f76549 into 3e3fac3bb
Pull Request #1392: Upgrade non-MQTT NuGet packages

873 of 1191 branches covered (73.3%)

Branch coverage included in aggregate %.

3408 of 3941 relevant lines covered (86.48%)

127.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.49
/src/Runtime/NetDaemon.Runtime/Internal/NetDaemonRuntime.cs
1
using System.Reactive.Linq;
2
using NetDaemon.AppModel;
3
using NetDaemon.HassModel;
4

5
namespace NetDaemon.Runtime.Internal;
6

7
internal class NetDaemonRuntime(IHomeAssistantRunner homeAssistantRunner,
18✔
8
        IOptions<HomeAssistantSettings> settings,
18✔
9
        IServiceProvider serviceProvider,
18✔
10
        ILogger<NetDaemonRuntime> logger,
18✔
11
        ICacheManager cacheManager)
18✔
12
    : IRuntime, INetDaemonRuntime
13
{
14
    private const string Version = "local build";
15
    private const int TimeoutInSeconds = 5;
16

17
    private readonly TaskCompletionSource _initializationTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
18✔
18

19
    private readonly HomeAssistantSettings _haSettings = settings.Value;
18✔
20

21
    private IAppModelContext? _applicationModelContext;
22
    private CancellationToken? _stoppingToken;
23
    private CancellationTokenSource? _runnerCancellationSource;
24

25
    public bool IsConnected;
26

27
    // These internals are used primarily for testing purposes
28
    internal IReadOnlyCollection<IApplication> ApplicationInstances =>
29
        _applicationModelContext?.Applications ?? [];
1!
30

31
    private Task _runnerTask = Task.CompletedTask;
18✔
32

33
    public void Start(CancellationToken stoppingToken)
34
    {
35
        logger.LogInformation("Starting NetDaemon runtime version {Version}.", Version);
18✔
36

37
        _stoppingToken = stoppingToken;
18✔
38

39
        homeAssistantRunner.OnConnect
18✔
40
            .Select(async c => await OnHomeAssistantClientConnected(c, stoppingToken).ConfigureAwait(false))
19✔
41
            .Subscribe();
18✔
42
        homeAssistantRunner.OnDisconnect
18✔
43
            .Select(async s => await OnHomeAssistantClientDisconnected(s).ConfigureAwait(false))
12✔
44
            .Subscribe();
18✔
45
        try
46
        {
47
            _runnerCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
18✔
48

49
            // Assign the runner so we can dispose it later. Note that this task contains the connection loop and will not end. We don't want to await it.
50
            _runnerTask = homeAssistantRunner.RunAsync(
18✔
51
                _haSettings.Host,
18✔
52
                _haSettings.Port,
18✔
53
                _haSettings.Ssl,
18✔
54
                _haSettings.Token,
18✔
55
                _haSettings.WebsocketPath,
18✔
56
                TimeSpan.FromSeconds(TimeoutInSeconds),
18✔
57
                _runnerCancellationSource.Token);
18✔
58

59
            // make sure we cancel the task if the stoppingToken is cancelled
60
            stoppingToken.Register(() => _initializationTcs.TrySetCanceled());
18✔
61
        }
18✔
62
        catch (OperationCanceledException)
×
63
        {
64
            // Ignore and just stop
65
        }
×
66
    }
18✔
67

68
    public Task WaitForInitializationAsync() => _initializationTcs.Task;
8✔
69

70
    private async Task OnHomeAssistantClientConnected(
71
        IHomeAssistantConnection haConnection,
72
        CancellationToken cancelToken)
73
    {
74
        try
75
        {
76
            logger.LogInformation("Successfully connected to Home Assistant");
19✔
77

78
            if (_applicationModelContext is not null)
19!
79
            {
80
                // Something wrong with unloading and disposing apps on restart of HA, we need to prevent apps loading multiple times
81
                logger.LogWarning("Applications were not successfully disposed during restart, skipping loading apps again");
×
82
                return;
×
83
            }
84

85
            IsConnected = true;
19✔
86

87
            await cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
19✔
88

89
            await LoadNewAppContextAsync(haConnection, cancelToken);
17✔
90

91
            // Signal anyone waiting that the runtime is now initialized
92
            _initializationTcs.TrySetResult();
17✔
93
        }
17✔
94
        catch (Exception ex)
2✔
95
        {
96
            if (!_initializationTcs.Task.IsCompleted)
2✔
97
            {
98
                // This means this was the first time we connected and StartAsync is still awaiting _startedAndConnected
99
                // By setting the exception on the task it will propagate up.
100
                _initializationTcs.SetException(ex);
1✔
101
            }
102
            logger.LogCritical(ex, "Error (re-)initializing after connect to Home Assistant");
2✔
103
        }
2✔
104
    }
19✔
105

106
    private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection, CancellationToken cancelToken)
107
    {
108
        var appModel = serviceProvider.GetService<IAppModel>();
17✔
109
        if (appModel == null) return;
17!
110

111
        _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
17✔
112

113
        // Handle state change for apps if registered
114
        var appStateHandler = serviceProvider.GetService<IHandleHomeAssistantAppStateUpdates>();
17✔
115
        if (appStateHandler == null) return;
26✔
116

117
        await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
8✔
118
    }
17✔
119

120
    private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason)
121
    {
122
        if (_stoppingToken?.IsCancellationRequested == true || reason == DisconnectReason.Client)
12!
123
        {
124
            logger.LogInformation("HassClient disconnected cause of user stopping");
4✔
125
        }
126
        else
127
        {
128
            var reasonString = reason switch
8!
129
            {
8✔
130
                DisconnectReason.Remote => "home assistant closed the connection",
8✔
131
                DisconnectReason.Error => "unknown error, set loglevel to debug to view details",
×
132
                DisconnectReason.Unauthorized => "token not authorized",
×
133
                DisconnectReason.NotReady => "home assistant not ready yet",
×
134
                _ => "unknown error"
×
135
            };
8✔
136
            logger.LogInformation("Home Assistant disconnected due to {Reason}",
8✔
137
                reasonString );
8✔
138
        }
139

140
        try
141
        {
142
            await DisposeApplicationsAsync().ConfigureAwait(false);
12✔
143
        }
12✔
144
        catch (Exception e)
×
145
        {
146
            logger.LogError(e, "Error disposing applications");
×
147
        }
×
148

149
        if (reason == DisconnectReason.Unauthorized)
12!
150
        {
151
            logger.LogInformation("Home Assistant runtime will dispose itself to stop automatic retrying to prevent user from being locked out.");
×
152
            await DisposeAsync();
×
153
        }
154

155
        IsConnected = false;
12✔
156
    }
12✔
157

158
    private async Task DisposeApplicationsAsync()
159
    {
160
        if (_applicationModelContext is not null)
21✔
161
        {
162
            await _applicationModelContext.DisposeAsync();
11✔
163

164
            _applicationModelContext = null;
11✔
165
        }
166
    }
21✔
167

168
    private volatile bool _isDisposed;
169
    public async ValueTask DisposeAsync()
170
    {
171
        if (_isDisposed) return;
17✔
172
        _isDisposed = true;
9✔
173

174
        await DisposeApplicationsAsync().ConfigureAwait(false);
9✔
175
        if (_runnerCancellationSource is not null)
9✔
176
            await _runnerCancellationSource.CancelAsync();
9✔
177
        try
178
        {
179
            await _runnerTask.ConfigureAwait(false);
9✔
180
        }
6✔
181
        catch (OperationCanceledException) { }
6✔
182
        _runnerCancellationSource?.Dispose();
9!
183
    }
13✔
184
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc