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

net-daemon / netdaemon / 14636304903

24 Apr 2025 07:50AM UTC coverage: 84.287% (+0.05%) from 84.239%
14636304903

Pull #1303

github

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

852 of 1133 branches covered (75.2%)

Branch coverage included in aggregate %.

23 of 27 new or added lines in 4 files covered. (85.19%)

11 existing lines in 2 files now uncovered.

3348 of 3850 relevant lines covered (86.96%)

136.34 hits per line

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

81.75
/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
    : INetDaemonRuntime
13
{
14
    private const string Version = "local build";
15
    private const int TimeoutInSeconds = 5;
16

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

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

22
    private Lazy<Task>? _initializationTask;
23
    private IAppModelContext? _applicationModelContext;
24
    private CancellationToken? _stoppingToken;
25
    private CancellationTokenSource? _runnerCancellationSource;
26

27
    public bool IsConnected;
28

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

33
    private Task _runnerTask = Task.CompletedTask;
18✔
34

35
    public Task EnsureInitializedAsync(CancellationToken stoppingToken = default)
36
    {
37
        if (_initializationTask is null)
26✔
38
        {
18✔
39
            lock (_initializationLock)
40
            {
41
                if (_initializationTask is null)
18✔
42
                {
43
                    _initializationTask = new Lazy<Task>(() => StartAndInitializeAsync(stoppingToken));
36✔
44
                }
45
            }
18✔
46
        }
47
        return _initializationTask.Value;
26✔
48
    }
49

50
    public AutoReconnectOptions AutoReconnectOptions { get; set; } = AutoReconnectOptions.StopReconnectOnUnAuthorized;
12✔
51

52
    private Task StartAndInitializeAsync(CancellationToken stoppingToken)
53
    {
54
        logger.LogInformation("Starting NetDaemon runtime version {Version}.", Version);
18✔
55

56
        _stoppingToken = stoppingToken;
18✔
57

58
        homeAssistantRunner.OnConnect
18✔
59
            .Select(async c => await OnHomeAssistantClientConnected(c, stoppingToken).ConfigureAwait(false))
19✔
60
            .Subscribe();
18✔
61
        homeAssistantRunner.OnDisconnect
18✔
62
            .Select(async s => await OnHomeAssistantClientDisconnected(s).ConfigureAwait(false))
12✔
63
            .Subscribe();
18✔
64
        try
65
        {
66
            _runnerCancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
18✔
67

68
            // 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.
69
            _runnerTask = homeAssistantRunner.RunAsync(
18✔
70
                _haSettings.Host,
18✔
71
                _haSettings.Port,
18✔
72
                _haSettings.Ssl,
18✔
73
                _haSettings.Token,
18✔
74
                _haSettings.WebsocketPath,
18✔
75
                TimeSpan.FromSeconds(TimeoutInSeconds),
18✔
76
                _runnerCancellationSource.Token);
18✔
77

78
            // make sure we cancel the task if the stoppingToken is cancelled
79
            stoppingToken.Register(() => _initializationTcs.TrySetCanceled());
18✔
80

81
            return _initializationTcs.Task;
18✔
82
        }
UNCOV
83
        catch (OperationCanceledException)
×
84
        {
85
            // Ignore and just stop
UNCOV
86
        }
×
NEW
UNCOV
87
        return Task.CompletedTask;
×
88
    }
18✔
89

90
    private async Task OnHomeAssistantClientConnected(
91
        IHomeAssistantConnection haConnection,
92
        CancellationToken cancelToken)
93
    {
94
        try
95
        {
96
            logger.LogInformation("Successfully connected to Home Assistant");
19✔
97

98
            if (_applicationModelContext is not null)
19!
99
            {
100
                // Something wrong with unloading and disposing apps on restart of HA, we need to prevent apps loading multiple times
NEW
UNCOV
101
                logger.LogWarning("Applications were not successfully disposed during restart, skipping loading apps again");
×
102
                return;
×
103
            }
104

105
            IsConnected = true;
19✔
106

107
            await cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
19✔
108

109
            await LoadNewAppContextAsync(haConnection, cancelToken);
17✔
110

111
            // Signal anyone waiting that the runtime is now initialized
112
            _initializationTcs.TrySetResult();
17✔
113
        }
17✔
114
        catch (Exception ex)
2✔
115
        {
116
            if (!_initializationTcs.Task.IsCompleted)
2✔
117
            {
118
                // This means this was the first time we connected and StartAsync is still awaiting _startedAndConnected
119
                // By setting the exception on the task it will propagate up.
120
                _initializationTcs.SetException(ex);
1✔
121
            }
122
            logger.LogCritical(ex, "Error (re-)initializing after connect to Home Assistant");
2✔
123
        }
2✔
124
    }
19✔
125

126
    private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection, CancellationToken cancelToken)
127
    {
128
        var appModel = serviceProvider.GetService<IAppModel>();
17✔
129
        if (appModel == null) return;
17!
130

131
        _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
17✔
132

133
        // Handle state change for apps if registered
134
        var appStateHandler = serviceProvider.GetService<IHandleHomeAssistantAppStateUpdates>();
17✔
135
        if (appStateHandler == null) return;
26✔
136

137
        await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
8✔
138
    }
17✔
139

140
    private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason)
141
    {
142
        if (_stoppingToken?.IsCancellationRequested == true || reason == DisconnectReason.Client)
12!
143
        {
144
            logger.LogInformation("HassClient disconnected cause of user stopping");
4✔
145
        }
146
        else
147
        {
148
            var reasonString = reason switch
8!
149
            {
8✔
150
                DisconnectReason.Remote => "home assistant closed the connection",
8✔
151
                DisconnectReason.Error => "unknown error, set loglevel to debug to view details",
×
152
                DisconnectReason.Unauthorized => "token not authorized",
×
UNCOV
153
                DisconnectReason.NotReady => "home assistant not ready yet",
×
154
                _ => "unknown error"
×
155
            };
8✔
156
            logger.LogInformation("Home Assistant disconnected due to {Reason}",
8✔
157
                reasonString );
8✔
158
        }
159

160
        try
161
        {
162
            await DisposeApplicationsAsync().ConfigureAwait(false);
12✔
163
        }
12✔
UNCOV
164
        catch (Exception e)
×
165
        {
UNCOV
166
            logger.LogError(e, "Error disposing applications");
×
UNCOV
167
        }
×
168

169
        if (AutoReconnectOptions == AutoReconnectOptions.StopReconnectOnUnAuthorized && reason == DisconnectReason.Unauthorized)
12!
170
        {
NEW
UNCOV
171
            logger.LogInformation("Home Assistant runtime will dispose itself to stop automatic retrying to prevent user from being locked out.");
×
NEW
UNCOV
172
            await DisposeAsync();
×
173
        }
174

175
        IsConnected = false;
12✔
176
    }
12✔
177

178
    private async Task DisposeApplicationsAsync()
179
    {
180
        if (_applicationModelContext is not null)
21✔
181
        {
182
            await _applicationModelContext.DisposeAsync();
11✔
183

184
            _applicationModelContext = null;
11✔
185
        }
186
    }
21✔
187

188
    private volatile bool _isDisposed;
189
    public async ValueTask DisposeAsync()
190
    {
191
        if (_isDisposed) return;
13✔
192
        _isDisposed = true;
9✔
193

194
        await DisposeApplicationsAsync().ConfigureAwait(false);
9✔
195
        if (_runnerCancellationSource is not null)
9✔
196
            await _runnerCancellationSource.CancelAsync();
9✔
197
        try
198
        {
199
            await _runnerTask.ConfigureAwait(false);
9✔
200
        }
6✔
201
        catch (OperationCanceledException) { }
6✔
202
        _runnerCancellationSource?.Dispose();
9!
203
    }
11✔
204
}
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