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

net-daemon / netdaemon / 26677597664

30 May 2026 07:04AM UTC coverage: 84.512% (+0.6%) from 83.944%
26677597664

Pull #1385

github

web-flow
Merge ca1957218 into 0116c9815
Pull Request #1385: MQTT Upgrade - attempt 2

906 of 1217 branches covered (74.45%)

Branch coverage included in aggregate %.

113 of 135 new or added lines in 5 files covered. (83.7%)

9 existing lines in 4 files now uncovered.

3514 of 4013 relevant lines covered (87.57%)

138.44 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,
18✔
6
    ILogger<IHomeAssistantRunner> logger) : IHomeAssistantRunner
18✔
7
{
8
    // The internal token source will make sure we
9
    // always cancel operations on dispose
10
    private readonly CancellationTokenSource _internalTokenSource = new();
18✔
11
    private readonly Subject<IHomeAssistantConnection> _onConnectSubject = new();
18✔
12

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

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

17
    private Task? _runTask;
18

19
    public IObservable<IHomeAssistantConnection> OnConnect => _onConnectSubject;
11✔
20
    public IObservable<DisconnectReason> OnDisconnect => _onDisconnectSubject;
15✔
21
    public IHomeAssistantConnection? CurrentConnection { get; internal set; }
328✔
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);
17✔
31
        return _runTask;
17✔
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
            {
UNCOV
45
                await Task.WhenAny(
×
UNCOV
46
                    _runTask,
×
UNCOV
47
                    Task.Delay(5000)
×
UNCOV
48
                ).ConfigureAwait(false);
×
UNCOV
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);
17✔
66
        var combinedToken = CancellationTokenSource.CreateLinkedTokenSource(_internalTokenSource.Token, cancelToken);
17✔
67
        while (!combinedToken.IsCancellationRequested)
17!
68
        {
69
            try
70
            {
71
                CurrentConnection = await client.ConnectAsync(host, port, ssl, token, websocketPath, combinedToken.Token)
17✔
72
                    .ConfigureAwait(false);
17✔
73
                // We successfully connected so lets reset the progressiveTimeout
74
                progressiveTimeout.Reset();
13✔
75

76
                // Start the event processing before publish the connection
77
                var eventsTask = CurrentConnection.WaitForConnectionToCloseAsync(combinedToken.Token);
13✔
78
                _onConnectSubject.OnNext(CurrentConnection);
13✔
79
                await eventsTask.ConfigureAwait(false);
13✔
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)
14✔
97
            {
98
                await DisposeConnectionAsync();
14✔
99
                if (_internalTokenSource.IsCancellationRequested)
14✔
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
13✔
107
                    ? DisconnectReason.Client
13✔
108
                    : DisconnectReason.Remote);
13✔
109
            }
13✔
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();
15✔
119

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

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

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