@@ -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