Skip to content

Commit 39fcb19

Browse files
committed
fix: TCP port monitor publish-state drift
1 parent 0f86605 commit 39fcb19

1 file changed

Lines changed: 52 additions & 11 deletions

File tree

Source/HomeAssistantLink.Monitors.TcpPort/TcpPortMonitor.cs

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ public sealed class TcpPortMonitor(
1414
IOptions<TcpPortMonitorConfig> options,
1515
ILogger<TcpPortMonitor> logger) : IMonitor
1616
{
17+
private static readonly TimeSpan stateRepublishInterval = TimeSpan.FromMinutes(5);
18+
1719
private static readonly Action<ILogger, string, string, int, Exception?> probeFailed =
1820
LoggerMessage.Define<string, string, int>(
1921
LogLevel.Debug,
@@ -31,7 +33,9 @@ public sealed class TcpPortMonitor(
3133
: options.Value ?? throw new ArgumentNullException(nameof(options));
3234

3335
private readonly ILogger<TcpPortMonitor> logger = logger ?? throw new ArgumentNullException(nameof(logger));
34-
private readonly ConcurrentDictionary<string, bool> lastStates = new(StringComparer.OrdinalIgnoreCase);
36+
37+
private readonly ConcurrentDictionary<string, PublishedState> publishedStates =
38+
new(StringComparer.OrdinalIgnoreCase);
3539

3640
private Func<EntityStateUpdate, CancellationToken, Task>? publishFunction;
3741
private CancellationTokenSource? cancellationTokenSource;
@@ -46,7 +50,11 @@ public Task StartAsync(Func<EntityStateUpdate, CancellationToken, Task> publish,
4650
this.publishFunction = publish ?? throw new ArgumentNullException(nameof(publish));
4751
this.cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
4852

49-
this.monitorTasks = [.. this.options.Targets.Select(target => this.MonitorTargetAsync(target, this.cancellationTokenSource.Token))];
53+
this.monitorTasks =
54+
[
55+
.. this.options.Targets.Select(target =>
56+
this.MonitorTargetAsync(target, this.cancellationTokenSource.Token)),
57+
];
5058

5159
return Task.CompletedTask;
5260
}
@@ -78,7 +86,7 @@ public async Task StopAsync(CancellationToken cancellationToken)
7886
this.cancellationTokenSource = null;
7987
this.monitorTasks = [];
8088
this.publishFunction = null;
81-
this.lastStates.Clear();
89+
this.publishedStates.Clear();
8290
}
8391

8492
private async Task MonitorTargetAsync(TcpPortTargetConfig target, CancellationToken cancellationToken)
@@ -100,21 +108,42 @@ private async Task CheckAndPublishAsync(
100108
{
101109
var isAvailable = await this.CheckPortAsync(target, cancellationToken).ConfigureAwait(false);
102110

103-
if (!forcePublish &&
104-
this.lastStates.TryGetValue(target.EntityId, out var previousState) &&
105-
previousState == isAvailable)
111+
var publishedState = this.publishedStates.GetOrAdd(
112+
target.EntityId,
113+
_ => new PublishedState());
114+
115+
var shouldPublish =
116+
forcePublish ||
117+
publishedState.LastPublishedValue != isAvailable ||
118+
this.ShouldRepublish(publishedState);
119+
120+
if (!shouldPublish)
106121
{
107122
return;
108123
}
109124

110-
this.lastStates[target.EntityId] = isAvailable;
111-
112125
var update = new EntityStateUpdate(
113126
target.EntityId,
114127
HomeAssistantEntityType.Boolean,
115128
isAvailable);
116129

117-
await this.PublishAsync(update, cancellationToken).ConfigureAwait(false);
130+
var published = await this.PublishAsync(update, cancellationToken).ConfigureAwait(false);
131+
132+
if (published)
133+
{
134+
publishedState.LastPublishedValue = isAvailable;
135+
publishedState.LastPublishedAt = DateTimeOffset.UtcNow;
136+
}
137+
}
138+
139+
private bool ShouldRepublish(PublishedState publishedState)
140+
{
141+
if (publishedState.LastPublishedValue == null)
142+
{
143+
return true;
144+
}
145+
146+
return DateTimeOffset.UtcNow - publishedState.LastPublishedAt >= stateRepublishInterval;
118147
}
119148

120149
private async Task<bool> CheckPortAsync(TcpPortTargetConfig target, CancellationToken cancellationToken)
@@ -149,22 +178,25 @@ private async Task<bool> CheckPortAsync(TcpPortTargetConfig target, Cancellation
149178
}
150179
}
151180

152-
private async Task PublishAsync(EntityStateUpdate update, CancellationToken cancellationToken)
181+
private async Task<bool> PublishAsync(EntityStateUpdate update, CancellationToken cancellationToken)
153182
{
154183
var callback = this.publishFunction;
155184

156185
if (callback == null)
157186
{
158-
return;
187+
return false;
159188
}
160189

161190
try
162191
{
163192
await callback(update, cancellationToken).ConfigureAwait(false);
193+
194+
return true;
164195
}
165196
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
166197
{
167198
// Expected during shutdown.
199+
return false;
168200
}
169201
catch (Exception exception)
170202
{
@@ -173,6 +205,8 @@ private async Task PublishAsync(EntityStateUpdate update, CancellationToken canc
173205
this.Name,
174206
update.EntityId,
175207
exception);
208+
209+
return false;
176210
}
177211
}
178212

@@ -223,4 +257,11 @@ private string GetTargetName(TcpPortTargetConfig target)
223257
? string.Create(CultureInfo.InvariantCulture, $"{target.Host}:{target.Port}")
224258
: target.Name;
225259
}
260+
261+
private sealed class PublishedState
262+
{
263+
public bool? LastPublishedValue { get; set; }
264+
265+
public DateTimeOffset LastPublishedAt { get; set; }
266+
}
226267
}

0 commit comments

Comments
 (0)