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

net-daemon / netdaemon / 14549288009

19 Apr 2025 12:49PM UTC coverage: 84.268% (+0.03%) from 84.239%
14549288009

Pull #1303

github

web-flow
Merge 9fdc72b86 into 91fddd674
Pull Request #1303: Feature/prevent blocking start async

842 of 1123 branches covered (74.98%)

Branch coverage included in aggregate %.

10 of 11 new or added lines in 4 files covered. (90.91%)

1 existing line in 1 file now uncovered.

3336 of 3835 relevant lines covered (86.99%)

980.9 hits per line

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

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

17
    private readonly TaskCompletionSource<object?> _initializationTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
18✔
18
    private readonly HomeAssistantSettings _haSettings = settings.Value;
18✔
19

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

24
    public bool IsConnected;
25

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

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

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

36
        _stoppingToken = stoppingToken;
18✔
37

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

48
            // 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.
49
            _runnerTask = homeAssistantRunner.RunAsync(
18✔
50
                _haSettings.Host,
18✔
51
                _haSettings.Port,
18✔
52
                _haSettings.Ssl,
18✔
53
                _haSettings.Token,
18✔
54
                _haSettings.WebsocketPath,
18✔
55
                TimeSpan.FromSeconds(TimeoutInSeconds),
18✔
56
                _runnerCancellationSource.Token);
18✔
57
        }
18✔
UNCOV
58
        catch (OperationCanceledException)
×
59
        {
60
            // Ignore and just stop
61
        }
×
62
    }
18✔
63

64
    private async Task OnHomeAssistantClientConnected(
65
        IHomeAssistantConnection haConnection,
66
        CancellationToken cancelToken)
67
    {
68
        try
69
        {
70
            logger.LogInformation("Successfully connected to Home Assistant");
19✔
71

72
            if (_applicationModelContext is not null)
19!
73
            {
74
                // Something wrong with unloading and disposing apps on restart of HA, we need to prevent apps loading multiple times
NEW
75
                logger.LogWarning("Applications were not successfully disposed during restart, skipping loading apps again");
×
76
                return;
×
77
            }
78

79
            IsConnected = true;
19✔
80

81
            await cacheManager.InitializeAsync(cancelToken).ConfigureAwait(false);
19✔
82

83
            await LoadNewAppContextAsync(haConnection, cancelToken);
17✔
84

85
            // Signal anyone waiting that the runtime is now initialized
86
            _initializationTcs.TrySetResult(null);
17✔
87
        }
17✔
88
        catch (Exception ex)
2✔
89
        {
90
            logger.LogCritical(ex, "Error re-initializing after reconnect to Home Assistant");
2✔
91
        }
2✔
92
    }
19✔
93

94
    private async Task LoadNewAppContextAsync(IHomeAssistantConnection haConnection, CancellationToken cancelToken)
95
    {
96
        var appModel = serviceProvider.GetService<IAppModel>();
17✔
97
        if (appModel == null) return;
17!
98

99
        _applicationModelContext = await appModel.LoadNewApplicationContext(CancellationToken.None).ConfigureAwait(false);
17✔
100

101
        // Handle state change for apps if registered
102
        var appStateHandler = serviceProvider.GetService<IHandleHomeAssistantAppStateUpdates>();
17✔
103
        if (appStateHandler == null) return;
26✔
104

105
        await appStateHandler.InitializeAsync(haConnection, _applicationModelContext);
8✔
106
    }
17✔
107

108
    private async Task OnHomeAssistantClientDisconnected(DisconnectReason reason)
109
    {
110
        if (_stoppingToken?.IsCancellationRequested == true || reason == DisconnectReason.Client)
12!
111
        {
112
            logger.LogInformation("HassClient disconnected cause of user stopping");
4✔
113
        }
114
        else
115
        {
116
            var reasonString = reason switch
8!
117
            {
8✔
118
                DisconnectReason.Remote => "home assistant closed the connection",
8✔
119
                DisconnectReason.Error => "unknown error, set loglevel to debug to view details",
×
120
                DisconnectReason.Unauthorized => "token not authorized",
×
121
                DisconnectReason.NotReady => "home assistant not ready yet",
×
122
                _ => "unknown error"
×
123
            };
8✔
124
            logger.LogInformation("Home Assistant disconnected due to {Reason}",
8✔
125
                reasonString );
8✔
126
        }
127

128
        try
129
        {
130
            await DisposeApplicationsAsync().ConfigureAwait(false);
12✔
131
        }
12✔
132
        catch (Exception e)
×
133
        {
134
            logger.LogError(e, "Error disposing applications");
×
135
        }
×
136
        IsConnected = false;
12✔
137
    }
12✔
138

139
    public Task EnsureInitializedAsync() => _initializationTcs.Task;
8✔
140

141
    private async Task DisposeApplicationsAsync()
142
    {
143
        if (_applicationModelContext is not null)
21✔
144
        {
145
            await _applicationModelContext.DisposeAsync();
11✔
146

147
            _applicationModelContext = null;
11✔
148
        }
149
    }
21✔
150

151
    private volatile bool _isDisposed;
152
    public async ValueTask DisposeAsync()
153
    {
154
        if (_isDisposed) return;
17✔
155
        _isDisposed = true;
9✔
156

157
        await DisposeApplicationsAsync().ConfigureAwait(false);
9✔
158
        if (_runnerCancellationSource is not null)
9✔
159
            await _runnerCancellationSource.CancelAsync();
9✔
160
        try
161
        {
162
            await _runnerTask.ConfigureAwait(false);
9✔
163
        }
6✔
164
        catch (OperationCanceledException) { }
6✔
165
        _runnerCancellationSource?.Dispose();
9!
166
    }
13✔
167
}
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