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

net-daemon / netdaemon / 18484065370

14 Sep 2025 02:22PM UTC coverage: 83.955% (-0.1%) from 84.073%
18484065370

push

github

web-flow
Upgrade the latest nuget packages (#1331)

885 of 1175 branches covered (75.32%)

Branch coverage included in aggregate %.

3369 of 3892 relevant lines covered (86.56%)

2513.03 hits per line

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

85.11
/src/Client/NetDaemon.HassClient/Internal/HomeAssistantRunner.cs
1
using NetDaemon.Client.Exceptions;
2

3
namespace NetDaemon.Client.Internal;
4

5
internal class HomeAssistantRunner(IHomeAssistantClient client,
16✔
6
    ILogger<IHomeAssistantRunner> logger) : IHomeAssistantRunner
16✔
7
{
8
    // The internal token source will make sure we
9
    // always cancel operations on dispose
10
    private readonly CancellationTokenSource _internalTokenSource = new();
16✔
11
    private readonly Subject<IHomeAssistantConnection> _onConnectSubject = new();
16✔
12

13
    private readonly Subject<DisconnectReason> _onDisconnectSubject = new();
16✔
14

15
    private readonly TimeSpan _maxTimeoutInSeconds = TimeSpan.FromSeconds(80);
16✔
16

17
    private Task? _runTask;
18

19
    public IObservable<IHomeAssistantConnection> OnConnect => _onConnectSubject;
9✔
20
    public IObservable<DisconnectReason> OnDisconnect => _onDisconnectSubject;
13✔
21
    public IHomeAssistantConnection? CurrentConnection { get; internal set; }
246✔
22

23
    public Task RunAsync(string host, int port, bool ssl, string token, TimeSpan timeout,
24
        CancellationToken cancelToken)
25
    {
26
        return RunAsync(host, port, ssl, token, HomeAssistantSettings.DefaultWebSocketPath, timeout, cancelToken);
×
27
    }
28
    public Task RunAsync(string host, int port, bool ssl, string token, string websocketPath, TimeSpan timeout, CancellationToken cancelToken)
29
    {
30
        _runTask = InternalRunAsync(host, port, ssl, token, websocketPath, timeout, cancelToken);
15✔
31
        return _runTask;
15✔
32
    }
33

34
    private bool _isDisposed;
35
    public async ValueTask DisposeAsync()
36
    {
37
        if (_isDisposed)
7!
38
            return;
×
39
        await _internalTokenSource.CancelAsync();
7✔
40

41
        if (_runTask?.IsCompleted == false)
7!
42
        {
43
            try
44
            {
45
                await Task.WhenAny(
×
46
                    _runTask,
×
47
                    Task.Delay(5000)
×
48
                ).ConfigureAwait(false);
×
49
            }
×
50
            catch
×
51
            {
52
                // Ignore errors
53
            }
×
54
        }
55

56
        _onConnectSubject.Dispose();
7✔
57
        _onDisconnectSubject.Dispose();
7✔
58
        _internalTokenSource.Dispose();
7✔
59
        _isDisposed = true;
7✔
60
    }
7✔
61

62
    private async Task InternalRunAsync(string host, int port, bool ssl, string token, string websocketPath, TimeSpan timeout,
63
        CancellationToken cancelToken)
64
    {
65
        var progressiveTimeout = new ProgressiveTimeout(timeout, _maxTimeoutInSeconds, 2.0);
15✔
66
        var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_internalTokenSource.Token, cancelToken);
15✔
67
        while (!combinedToken.IsCancellationRequested)
15!
68
        {
69
            try
70
            {
71
                CurrentConnection = await client.ConnectAsync(host, port, ssl, token, websocketPath, combinedToken.Token)
15✔
72
                    .ConfigureAwait(false);
15✔
73
                // We successfully connected so lets reset the progressiveTimeout
74
                progressiveTimeout.Reset();
11✔
75

76
                // Start the event processing before publish the connection
77
                var eventsTask = CurrentConnection.WaitForConnectionToCloseAsync(combinedToken.Token);
11✔
78
                _onConnectSubject.OnNext(CurrentConnection);
11✔
79
                await eventsTask.ConfigureAwait(false);
11✔
80
            }
×
81
            catch (HomeAssistantConnectionException de) when (de.Reason == DisconnectReason.Unauthorized)
2✔
82
            {
83
                logger.LogError("User token unauthorized! Will not retry connecting...");
1✔
84
                await DisposeConnectionAsync();
1✔
85
                _onDisconnectSubject.OnNext(DisconnectReason.Unauthorized);
1✔
86
                return;
1✔
87
            }
88
            catch (HomeAssistantConnectionException de) when (de.Reason == DisconnectReason.NotReady)
1✔
89
            {
90
                logger.LogInformation("Home Assistant is not ready yet!");
1✔
91
                await DisposeConnectionAsync();
1✔
92
                // We have an connection but waiting for Home Assistant to be ready so lets reset the progressiveTimeout
93
                progressiveTimeout.Reset();
1✔
94
                _onDisconnectSubject.OnNext(DisconnectReason.NotReady);
1✔
95
            }
1✔
96
            catch (OperationCanceledException)
12✔
97
            {
98
                await DisposeConnectionAsync();
12✔
99
                if (_internalTokenSource.IsCancellationRequested)
12✔
100
                {
101
                    // We have internal cancellation due to dispose
102
                    // just return without any further due
103
                    return;
1✔
104
                }
105

106
                _onDisconnectSubject.OnNext(cancelToken.IsCancellationRequested
11✔
107
                    ? DisconnectReason.Client
11✔
108
                    : DisconnectReason.Remote);
11✔
109
            }
11✔
110
            catch (Exception e)
1✔
111
            {
112
                // In most cases this is just normal when client fails to connect so we log as debug
113
                logger.LogDebug(e, "Unhandled exception connecting to Home Assistant!");
1✔
114
                await DisposeConnectionAsync();
1✔
115
                _onDisconnectSubject.OnNext(DisconnectReason.Error);
1✔
116
            }
117

118
            await DisposeConnectionAsync();
13✔
119

120
            if (combinedToken.IsCancellationRequested)
13✔
121
                return; // If we are cancelled we should not retry
2✔
122

123
            var waitTimeout = progressiveTimeout.Timeout;
11✔
124
            logger.LogInformation("Client connection failed, retrying in {Seconds} seconds...", waitTimeout.TotalSeconds);
11✔
125
            await Task.Delay(waitTimeout, combinedToken.Token).ConfigureAwait(false);
11✔
126
        }
127
    }
4✔
128

129
    private async Task DisposeConnectionAsync()
130
    {
131
        if (CurrentConnection is not null)
28✔
132
        {
133
            // Just try to dispose the connection silently
134
            try
135
            {
136
                await CurrentConnection.DisposeAsync().ConfigureAwait(false);
11✔
137
            }
11✔
138
            finally
139
            {
140
                CurrentConnection = null;
11✔
141
            }
142
        }
143
    }
28✔
144
}
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