Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,31 @@ namespace CodeCasa.AutomationPipelines.Lights.Extensions
public static class LightTransitionNodeExtensions
{
/// <summary>
/// Creates a timeout node that automatically turns off the light after the specified time span.
/// The timeout is not reset by any external events.
/// Wraps the pipeline node to automatically transition to an 'Off' state after a specified duration of inactivity.
/// </summary>
/// <param name="node">The pipeline node to wrap with timeout behavior.</param>
/// <param name="timeSpan">The duration after which the light will turn off.</param>
/// <param name="scheduler">The scheduler to use for timing operations.</param>
/// <returns>A new pipeline node that wraps the original node with timeout behavior.</returns>
/// <param name="node">The source pipeline node.</param>
/// <param name="timeSpan">The duration to wait before turning off.</param>
/// <param name="scheduler">The scheduler used for timing operations.</param>
/// <returns>A new pipeline node that times out to 'Off'.</returns>
public static IPipelineNode<LightTransition> TurnOffAfter(this IPipelineNode<LightTransition> node,
TimeSpan timeSpan, IScheduler scheduler)
{
return new ResettableTimeoutNode<Unit>(node, timeSpan, Observable.Empty<Unit>(), scheduler);
return new ResettableTimeoutNode(node, timeSpan, Observable.Empty<bool>(), scheduler);
}

/// <summary>
/// Creates a timeout node that automatically turns off the light after the specified time span.
/// The timeout can be reset when the observable emits a value.
/// Wraps the pipeline node to automatically transition to an 'Off' state after a specified duration of inactivity,
/// with an optional observable to persist the current state and bypass the timeout.
/// </summary>
/// <typeparam name="T">The type of values emitted by the reset timer observable.</typeparam>
/// <param name="node">The pipeline node to wrap with timeout behavior.</param>
/// <param name="timeSpan">The duration after which the light will turn off.</param>
/// <param name="resetTimerObservable">An observable that resets the timeout timer when it emits a value.</param>
/// <param name="scheduler">The scheduler to use for timing operations.</param>
/// <returns>A new pipeline node that wraps the original node with resettable timeout behavior.</returns>
public static IPipelineNode<LightTransition> TurnOffAfter<T>(this IPipelineNode<LightTransition> node,
TimeSpan timeSpan, IObservable<T> resetTimerObservable, IScheduler scheduler)
/// <param name="node">The source pipeline node.</param>
/// <param name="timeSpan">The duration to wait before turning off.</param>
/// <param name="persistObservable">An observable that, when true, prevents the timeout from triggering.</param>
/// <param name="scheduler">The scheduler used for timing operations.</param>
/// <returns>A new pipeline node that times out to 'Off' unless persistence is active.</returns>
public static IPipelineNode<LightTransition> TurnOffAfter(this IPipelineNode<LightTransition> node,
TimeSpan timeSpan, IObservable<bool> persistObservable, IScheduler scheduler)
{
return new ResettableTimeoutNode<T>(node, timeSpan, resetTimerObservable, scheduler);
return new ResettableTimeoutNode(node, timeSpan, persistObservable, scheduler);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ internal static IServiceScope CreateLightContextScope<TLight>(this IServiceProvi
}

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

/// <summary>
/// Creates a pipeline node that applies the specified light parameters and automatically turns off the light after a specified duration.
/// The timeout can be reset when the observable emits a value.
/// Creates a pipeline node that applies the specified light parameters and automatically transitions
/// to an 'Off' state after a specified duration of inactivity.
/// </summary>
/// <typeparam name="T">The type of elements emitted by the reset timer observable.</typeparam>
/// <param name="serviceProvider">The service provider.</param>
/// <param name="serviceProvider">The service provider used to resolve the <see cref="IScheduler"/>.</param>
/// <param name="lightParameters">The light parameters to apply as a transition.</param>
/// <param name="timeSpan">The duration after which the light should turn off.</param>
/// <param name="resetTimerObservable">An observable that resets the turn-off timer when it emits.</param>
/// <returns>A pipeline node that applies the light parameters and handles the turn-off behavior.</returns>
public static IPipelineNode<LightTransition> CreateAutoOffLightNode<T>(this IServiceProvider serviceProvider,
/// <param name="timeSpan">The duration to wait before turning off.</param>
/// <param name="persistObservable">An observable that, when true, prevents the timeout from triggering.</param>
/// <returns>A pipeline node that applies the light parameters and manages the turn-off timeout.</returns>
public static IPipelineNode<LightTransition> CreateAutoOffLightNode(this IServiceProvider serviceProvider,
LightParameters lightParameters,
TimeSpan timeSpan, IObservable<T> resetTimerObservable)
TimeSpan timeSpan, IObservable<bool> persistObservable)
{
var scheduler = serviceProvider.GetRequiredService<IScheduler>();
var innerNode = new StaticLightTransitionNode(lightParameters.AsTransition(), scheduler);
return innerNode.TurnOffAfter(timeSpan, resetTimerObservable, scheduler);
return innerNode.TurnOffAfter(timeSpan, persistObservable, scheduler);
}
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
using System.Reactive;
using CodeCasa.Lights;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Disposables.Fluent;
using System.Reactive.Linq;
using CodeCasa.Lights;

namespace CodeCasa.AutomationPipelines.Lights.Nodes
{
internal class ResettableTimeoutNode<T> : LightTransitionNode{
internal class ResettableTimeoutNode : LightTransitionNode, IDisposable
{
private readonly CompositeDisposable _disposables = new();
private IDisposable? _timerSubscription;
private bool _isPersisting;

public ResettableTimeoutNode(IPipelineNode<LightTransition> childNode, TimeSpan turnOffTime,
IObservable<T> refreshObservable, IScheduler scheduler) : base(scheduler)
IObservable<bool> persistObservable, IScheduler scheduler) : base(scheduler)
{
var serializedChild = childNode.OnNewOutput.Prepend(childNode.Output).ObserveOn(scheduler);
childNode.OnNewOutput
.Prepend(childNode.Output)
.ObserveOn(scheduler)
.Subscribe(output =>
{
Output = output;
RestartTimer();
}).DisposeWith(_disposables);

var serializedTurnOff =
refreshObservable.Select(_ => Unit.Default)
.Prepend(Unit.Default)
.Throttle(turnOffTime, scheduler)
.Take(1)
.ObserveOn(scheduler);
persistObservable
.ObserveOn(scheduler)
.DistinctUntilChanged()
.Subscribe(persist =>
{
_isPersisting = persist;
if (persist)
{
_timerSubscription?.Dispose();
}
else
{
RestartTimer();
}
}).DisposeWith(_disposables);

serializedChild
.TakeUntil(serializedTurnOff)
.Subscribe(output => { Output = output; });
void RestartTimer()
{
if (_isPersisting)
{
return;
}

serializedTurnOff
.Subscribe(_ => { ChangeOutputAndTurnOnPassThroughOnNextInput(LightTransition.Off()); });
_timerSubscription?.Dispose();
_timerSubscription = Observable.Timer(turnOffTime, scheduler)
.Subscribe(_ => ChangeOutputAndTurnOnPassThroughOnNextInput(LightTransition.Off()));
}
}

public void Dispose() => _disposables.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Reactive.Testing" Version="6.1.0" />
<PackageReference Include="MSTest" Version="4.1.0" />
<PackageReference Include="Moq" Version="4.20.72" />
</ItemGroup>
Expand Down
Loading