Skip to content

Commit 5473b39

Browse files
authored
Allow ResettableTimeoutNode to keep its state. (#207)
1 parent f7d6000 commit 5473b39

5 files changed

Lines changed: 382 additions & 49 deletions

File tree

src/CodeCasa.AutomationPipelines.Lights/Extensions/LightTransitionNodeExtensions.cs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,31 @@ namespace CodeCasa.AutomationPipelines.Lights.Extensions
1313
public static class LightTransitionNodeExtensions
1414
{
1515
/// <summary>
16-
/// Creates a timeout node that automatically turns off the light after the specified time span.
17-
/// The timeout is not reset by any external events.
16+
/// Wraps the pipeline node to automatically transition to an 'Off' state after a specified duration of inactivity.
1817
/// </summary>
19-
/// <param name="node">The pipeline node to wrap with timeout behavior.</param>
20-
/// <param name="timeSpan">The duration after which the light will turn off.</param>
21-
/// <param name="scheduler">The scheduler to use for timing operations.</param>
22-
/// <returns>A new pipeline node that wraps the original node with timeout behavior.</returns>
18+
/// <param name="node">The source pipeline node.</param>
19+
/// <param name="timeSpan">The duration to wait before turning off.</param>
20+
/// <param name="scheduler">The scheduler used for timing operations.</param>
21+
/// <returns>A new pipeline node that times out to 'Off'.</returns>
2322
public static IPipelineNode<LightTransition> TurnOffAfter(this IPipelineNode<LightTransition> node,
2423
TimeSpan timeSpan, IScheduler scheduler)
2524
{
26-
return new ResettableTimeoutNode<Unit>(node, timeSpan, Observable.Empty<Unit>(), scheduler);
25+
return new ResettableTimeoutNode(node, timeSpan, Observable.Empty<bool>(), scheduler);
2726
}
2827

2928
/// <summary>
30-
/// Creates a timeout node that automatically turns off the light after the specified time span.
31-
/// The timeout can be reset when the observable emits a value.
29+
/// Wraps the pipeline node to automatically transition to an 'Off' state after a specified duration of inactivity,
30+
/// with an optional observable to persist the current state and bypass the timeout.
3231
/// </summary>
33-
/// <typeparam name="T">The type of values emitted by the reset timer observable.</typeparam>
34-
/// <param name="node">The pipeline node to wrap with timeout behavior.</param>
35-
/// <param name="timeSpan">The duration after which the light will turn off.</param>
36-
/// <param name="resetTimerObservable">An observable that resets the timeout timer when it emits a value.</param>
37-
/// <param name="scheduler">The scheduler to use for timing operations.</param>
38-
/// <returns>A new pipeline node that wraps the original node with resettable timeout behavior.</returns>
39-
public static IPipelineNode<LightTransition> TurnOffAfter<T>(this IPipelineNode<LightTransition> node,
40-
TimeSpan timeSpan, IObservable<T> resetTimerObservable, IScheduler scheduler)
32+
/// <param name="node">The source pipeline node.</param>
33+
/// <param name="timeSpan">The duration to wait before turning off.</param>
34+
/// <param name="persistObservable">An observable that, when true, prevents the timeout from triggering.</param>
35+
/// <param name="scheduler">The scheduler used for timing operations.</param>
36+
/// <returns>A new pipeline node that times out to 'Off' unless persistence is active.</returns>
37+
public static IPipelineNode<LightTransition> TurnOffAfter(this IPipelineNode<LightTransition> node,
38+
TimeSpan timeSpan, IObservable<bool> persistObservable, IScheduler scheduler)
4139
{
42-
return new ResettableTimeoutNode<T>(node, timeSpan, resetTimerObservable, scheduler);
40+
return new ResettableTimeoutNode(node, timeSpan, persistObservable, scheduler);
4341
}
4442
}
4543
}

src/CodeCasa.AutomationPipelines.Lights/Extensions/ServiceProviderExtensions.cs

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ internal static IServiceScope CreateLightContextScope<TLight>(this IServiceProvi
2626
}
2727

2828
/// <summary>
29-
/// Creates a pipeline node that applies the specified light parameters and automatically turns off the light after a specified duration.
30-
/// The timeout is not reset by any external events.
29+
/// Creates a pipeline node that applies the specified light parameters and automatically transitions
30+
/// to an 'Off' state after a specified duration of inactivity.
3131
/// </summary>
32-
/// <param name="serviceProvider">The service provider.</param>
32+
/// <param name="serviceProvider">The service provider used to resolve the <see cref="IScheduler"/>.</param>
3333
/// <param name="lightParameters">The light parameters to apply as a transition.</param>
34-
/// <param name="timeSpan">The duration after which the light should turn off.</param>
35-
/// <returns>A pipeline node that applies the light parameters and handles the turn-off behavior.</returns>
34+
/// <param name="timeSpan">The duration to wait before turning off.</param>
35+
/// <returns>A pipeline node that applies the light parameters and manages the turn-off timeout.</returns>
3636
public static IPipelineNode<LightTransition> CreateAutoOffLightNode(this IServiceProvider serviceProvider,
3737
LightParameters lightParameters,
3838
TimeSpan timeSpan)
@@ -43,21 +43,20 @@ public static IPipelineNode<LightTransition> CreateAutoOffLightNode(this IServic
4343
}
4444

4545
/// <summary>
46-
/// Creates a pipeline node that applies the specified light parameters and automatically turns off the light after a specified duration.
47-
/// The timeout can be reset when the observable emits a value.
46+
/// Creates a pipeline node that applies the specified light parameters and automatically transitions
47+
/// to an 'Off' state after a specified duration of inactivity.
4848
/// </summary>
49-
/// <typeparam name="T">The type of elements emitted by the reset timer observable.</typeparam>
50-
/// <param name="serviceProvider">The service provider.</param>
49+
/// <param name="serviceProvider">The service provider used to resolve the <see cref="IScheduler"/>.</param>
5150
/// <param name="lightParameters">The light parameters to apply as a transition.</param>
52-
/// <param name="timeSpan">The duration after which the light should turn off.</param>
53-
/// <param name="resetTimerObservable">An observable that resets the turn-off timer when it emits.</param>
54-
/// <returns>A pipeline node that applies the light parameters and handles the turn-off behavior.</returns>
55-
public static IPipelineNode<LightTransition> CreateAutoOffLightNode<T>(this IServiceProvider serviceProvider,
51+
/// <param name="timeSpan">The duration to wait before turning off.</param>
52+
/// <param name="persistObservable">An observable that, when true, prevents the timeout from triggering.</param>
53+
/// <returns>A pipeline node that applies the light parameters and manages the turn-off timeout.</returns>
54+
public static IPipelineNode<LightTransition> CreateAutoOffLightNode(this IServiceProvider serviceProvider,
5655
LightParameters lightParameters,
57-
TimeSpan timeSpan, IObservable<T> resetTimerObservable)
56+
TimeSpan timeSpan, IObservable<bool> persistObservable)
5857
{
5958
var scheduler = serviceProvider.GetRequiredService<IScheduler>();
6059
var innerNode = new StaticLightTransitionNode(lightParameters.AsTransition(), scheduler);
61-
return innerNode.TurnOffAfter(timeSpan, resetTimerObservable, scheduler);
60+
return innerNode.TurnOffAfter(timeSpan, persistObservable, scheduler);
6261
}
6362
}
Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,58 @@
1-
using System.Reactive;
1+
using CodeCasa.Lights;
22
using System.Reactive.Concurrency;
3+
using System.Reactive.Disposables;
4+
using System.Reactive.Disposables.Fluent;
35
using System.Reactive.Linq;
4-
using CodeCasa.Lights;
56

67
namespace CodeCasa.AutomationPipelines.Lights.Nodes
78
{
8-
internal class ResettableTimeoutNode<T> : LightTransitionNode{
9+
internal class ResettableTimeoutNode : LightTransitionNode, IDisposable
10+
{
11+
private readonly CompositeDisposable _disposables = new();
12+
private IDisposable? _timerSubscription;
13+
private bool _isPersisting;
14+
915
public ResettableTimeoutNode(IPipelineNode<LightTransition> childNode, TimeSpan turnOffTime,
10-
IObservable<T> refreshObservable, IScheduler scheduler) : base(scheduler)
16+
IObservable<bool> persistObservable, IScheduler scheduler) : base(scheduler)
1117
{
12-
var serializedChild = childNode.OnNewOutput.Prepend(childNode.Output).ObserveOn(scheduler);
18+
childNode.OnNewOutput
19+
.Prepend(childNode.Output)
20+
.ObserveOn(scheduler)
21+
.Subscribe(output =>
22+
{
23+
Output = output;
24+
RestartTimer();
25+
}).DisposeWith(_disposables);
1326

14-
var serializedTurnOff =
15-
refreshObservable.Select(_ => Unit.Default)
16-
.Prepend(Unit.Default)
17-
.Throttle(turnOffTime, scheduler)
18-
.Take(1)
19-
.ObserveOn(scheduler);
27+
persistObservable
28+
.ObserveOn(scheduler)
29+
.DistinctUntilChanged()
30+
.Subscribe(persist =>
31+
{
32+
_isPersisting = persist;
33+
if (persist)
34+
{
35+
_timerSubscription?.Dispose();
36+
}
37+
else
38+
{
39+
RestartTimer();
40+
}
41+
}).DisposeWith(_disposables);
2042

21-
serializedChild
22-
.TakeUntil(serializedTurnOff)
23-
.Subscribe(output => { Output = output; });
43+
void RestartTimer()
44+
{
45+
if (_isPersisting)
46+
{
47+
return;
48+
}
2449

25-
serializedTurnOff
26-
.Subscribe(_ => { ChangeOutputAndTurnOnPassThroughOnNextInput(LightTransition.Off()); });
50+
_timerSubscription?.Dispose();
51+
_timerSubscription = Observable.Timer(turnOffTime, scheduler)
52+
.Subscribe(_ => ChangeOutputAndTurnOnPassThroughOnNextInput(LightTransition.Off()));
53+
}
2754
}
55+
56+
public void Dispose() => _disposables.Dispose();
2857
}
2958
}

tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11+
<PackageReference Include="Microsoft.Reactive.Testing" Version="6.1.0" />
1112
<PackageReference Include="MSTest" Version="4.1.0" />
1213
<PackageReference Include="Moq" Version="4.20.72" />
1314
</ItemGroup>

0 commit comments

Comments
 (0)