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

net-daemon / netdaemon / 14758005417

30 Apr 2025 03:12PM UTC coverage: 84.278% (+0.04%) from 84.239%
14758005417

Pull #1303

github

web-flow
Merge d4dd69e8f into 2e3bb120d
Pull Request #1303: Feature/prevent blocking start async

848 of 1129 branches covered (75.11%)

Branch coverage included in aggregate %.

16 of 19 new or added lines in 4 files covered. (84.21%)

9 existing lines in 1 file now uncovered.

3344 of 3845 relevant lines covered (86.97%)

670.01 hits per line

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

80.95
/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 AutoReconnectOptions AutoReconnectOptions { get; set; } = AutoReconnectOptions.StopReconnectOnUnAuthorized;
12✔
34

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

39
        _stoppingToken = stoppingToken;
18✔
40

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

51
            // 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.
52
            _runnerTask = homeAssistantRunner.RunAsync(
18✔
53
                _haSettings.Host,
18✔
54
                _haSettings.Port,
18✔
55
                _haSettings.Ssl,
18✔
56
                _haSettings.Token,
18✔
57
                _haSettings.WebsocketPath,
18✔
58
                TimeSpan.FromSeconds(TimeoutInSeconds),
18✔
59
                _runnerCancellationSource.Token);
18✔
60

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

70
    public Task WaitForInitializationAsync() => _initializationTcs.Task;
8✔
71

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

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

87
            IsConnected = true;
19✔
88

89
            await cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
19✔
90

91
            await LoadNewAppContextAsync(haConnection, cancelToken);
17✔
92

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

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

113
        _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
17✔
114

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

119
        await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
8✔
120
    }
17✔
121

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

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

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

157
        IsConnected = false;
12✔
158
    }
12✔
159

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

166
            _applicationModelContext = null;
11✔
167
        }
168
    }
21✔
169

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

176
        await DisposeApplicationsAsync().ConfigureAwait(false);
9✔
177
        if (_runnerCancellationSource is not null)
9✔
178
            await _runnerCancellationSource.CancelAsync();
9✔
179
        try
180
        {
181
            await _runnerTask.ConfigureAwait(false);
9✔
182
        }
6✔
183
        catch (OperationCanceledException) { }
6✔
184
        _runnerCancellationSource?.Dispose();
9!
185
    }
13✔
186
}
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