Skip to content

Commit 0bfa4cb

Browse files
authored
Implemented grace period for toggle logic. (#233)
* Cleanup casts * Implemented LastChangedUtc and LastUpdatedUtc in ILight * Implemented grace period in toggle logic. * Fix in grace period logic.
1 parent 6d4e752 commit 0bfa4cb

11 files changed

Lines changed: 108 additions & 28 deletions

src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Scene.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,11 @@ public static ILightTransitionPipelineConfigurator<NetDaemonLight> AddToggle<T>(
203203
IObservable<T> triggerObservable,
204204
params IEntityCore[] sceneEntities)
205205
{
206-
return configurator.AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<NetDaemonLight>>)(c =>
206+
return configurator.AddToggle(triggerObservable, c =>
207207
{
208208
foreach (var scene in sceneEntities)
209209
c.AddScene(scene);
210-
}));
210+
});
211211
}
212212

213213
// -------------------------------------------------------------------------
@@ -230,11 +230,11 @@ public static ILightTransitionPipelineConfigurator<NetDaemonLight> AddCycle<T>(
230230
IObservable<T> triggerObservable,
231231
params IEntityCore[] sceneEntities)
232232
{
233-
return configurator.AddCycle(triggerObservable, (Action<ILightTransitionCycleConfigurator<NetDaemonLight>>)(c =>
233+
return configurator.AddCycle(triggerObservable, c =>
234234
{
235235
foreach (var scene in sceneEntities)
236236
c.AddScene(scene);
237-
}));
237+
});
238238
}
239239

240240
}

src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.Scene.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ public static ILightTransitionReactiveNodeConfigurator<NetDaemonLight> AddToggle
5252
IObservable<T> triggerObservable,
5353
params IEntityCore[] sceneEntities)
5454
{
55-
return configurator.AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<NetDaemonLight>>)(c =>
55+
return configurator.AddToggle(triggerObservable, c =>
5656
{
5757
foreach (var scene in sceneEntities)
5858
c.AddScene(scene);
59-
}));
59+
});
6060
}
6161

6262
// -------------------------------------------------------------------------
@@ -79,11 +79,11 @@ public static ILightTransitionReactiveNodeConfigurator<NetDaemonLight> AddCycle<
7979
IObservable<T> triggerObservable,
8080
params IEntityCore[] sceneEntities)
8181
{
82-
return configurator.AddCycle(triggerObservable, (Action<ILightTransitionCycleConfigurator<NetDaemonLight>>)(c =>
82+
return configurator.AddCycle(triggerObservable, c =>
8383
{
8484
foreach (var scene in sceneEntities)
8585
c.AddScene(scene);
86-
}));
86+
});
8787
}
8888

8989
}

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public static IObservable<TValue> ToCycleObservable<TTrigger, TValue>(
3535

3636
public static IObservable<TValue?> ToToggleObservable<TTrigger, TValue>(
3737
this IObservable<TTrigger> triggerObservable,
38-
Func<bool> offCondition,
38+
Func<DateTime?, bool> offCondition,
3939
Func<TValue> offValueFactory,
4040
IEnumerable<Func<TValue>> valueFactories,
4141
TimeSpan timeout,
@@ -55,19 +55,20 @@ public static IObservable<TValue> ToCycleObservable<TTrigger, TValue>(
5555
{
5656
var utcNow = DateTime.UtcNow;
5757
var consecutive = previousLastChanged != null && utcNow - previousLastChanged < timeout;
58-
previousLastChanged = utcNow;
59-
58+
6059
if (!consecutive)
6160
{
6261
index = 0;
63-
if (offCondition())
62+
if (offCondition(previousLastChanged))
6463
{
64+
previousLastChanged = utcNow;
6565
return offValueFactory();
6666
}
6767
}
6868

6969
var value = index >= valueFactoryArray.Length ? offValueFactory() : valueFactoryArray[index]();
7070
index = index < maxIndexValue ? index + 1 : 0;
71+
previousLastChanged = utcNow;
7172
return value;
7273
});
7374
}

src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.Reactive.Toggle.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,6 @@ public ILightTransitionPipelineConfigurator<TLight> AddToggle<T>(IObservable<T>
9090
public ILightTransitionPipelineConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
9191
Action<ITimelineConfigurator> configure)
9292
{
93-
return AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
93+
return AddToggle(triggerObservable, c => c.AddTimeline(configure));
9494
}
9595
}

src/CodeCasa.AutomationPipelines.Lights/ReactiveNode/CompositeLightTransitionReactiveNodeConfigurator.Toggle.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,20 +65,37 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
6565
{
6666
var toggleConfigurators = configurators.ToDictionary(kvp => kvp.Key,
6767
kvp => new LightTransitionToggleConfigurator<TLight>(kvp.Value.Light, scheduler));
68-
var compositeCycleConfigurator = new CompositeLightTransitionToggleConfigurator<TLight>(toggleConfigurators, []);
69-
configure(compositeCycleConfigurator);
68+
var compositeToggleConfigurator = new CompositeLightTransitionToggleConfigurator<TLight>(toggleConfigurators, []);
69+
configure(compositeToggleConfigurator);
7070
var shareableTriggerObservable = _observableSharingStrategy.Apply(triggerObservable);
71-
configurators.ForEach(kvp => kvp.Value.AddNodeSource(shareableTriggerObservable.ToToggleObservable(
72-
() => configurators.Values.Any(c => c.Light.IsOn()),
73-
() => new TurnOffThenPassThroughNode(),
74-
toggleConfigurators[kvp.Key].NodeFactories.Select(fact =>
75-
{
76-
return new Func<IPipelineNode<LightTransition>>(() =>
77-
fact.CreateScopedNode(kvp.Value.ServiceProvider) // Note: This service provider already has the light registered. We scope it further for node lifetime.
71+
72+
configurators.ForEach(kvp =>
73+
{
74+
var toggleConfig = toggleConfigurators[kvp.Key];
75+
var gracePeriod = toggleConfig.GracePeriod ?? TimeSpan.FromSeconds(1);
76+
kvp.Value.AddNodeSource(shareableTriggerObservable.ToToggleObservable(
77+
lastActivationTime =>
78+
{
79+
var utcNow = DateTime.UtcNow;
80+
if (utcNow - kvp.Value.Light.LastChangedUtc <= gracePeriod &&
81+
(!lastActivationTime.HasValue || utcNow - lastActivationTime > gracePeriod))
82+
{
83+
return !configurators.Values.Any(c => c.Light.IsOn());
84+
}
85+
86+
return configurators.Values.Any(c => c.Light.IsOn());
87+
},
88+
() => new TurnOffThenPassThroughNode(),
89+
toggleConfig.NodeFactories.Select(fact =>
90+
{
91+
return new Func<IPipelineNode<LightTransition>>(() =>
92+
fact.CreateScopedNode(kvp.Value
93+
.ServiceProvider) // Note: This service provider already has the light registered. We scope it further for node lifetime.
7894
);
79-
}),
80-
toggleConfigurators[kvp.Key].ToggleTimeout ?? TimeSpan.FromMilliseconds(1000),
81-
toggleConfigurators[kvp.Key].IncludeOffValue)));
95+
}),
96+
toggleConfig.ToggleTimeout ?? TimeSpan.FromMilliseconds(1000),
97+
toggleConfig.IncludeOffValue));
98+
});
8299
return this;
83100
}
84101

@@ -96,5 +113,5 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
96113
/// <inheritdoc/>
97114
public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
98115
Action<ITimelineConfigurator> configure)
99-
=> AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
116+
=> AddToggle(triggerObservable, c => c.AddTimeline(configure));
100117
}

src/CodeCasa.AutomationPipelines.Lights/ReactiveNode/LightTransitionReactiveNodeConfigurator.Toggle.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,20 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
6565
{
6666
var toggleConfigurator = new LightTransitionToggleConfigurator<TLight>(Light, _scheduler);
6767
configure(toggleConfigurator);
68+
69+
var gracePeriod = toggleConfigurator.GracePeriod ?? TimeSpan.FromSeconds(1);
6870
AddNodeSource(triggerObservable.ToToggleObservable(
69-
() => Light.IsOn(),
71+
lastActivationTime =>
72+
{
73+
var utcNow = DateTime.UtcNow;
74+
if (utcNow - Light.LastChangedUtc <= gracePeriod &&
75+
(!lastActivationTime.HasValue || utcNow - lastActivationTime > gracePeriod))
76+
{
77+
return !Light.IsOn();
78+
}
79+
80+
return Light.IsOn();
81+
},
7082
() => new TurnOffThenPassThroughNode(),
7183
toggleConfigurator.NodeFactories.Select(fact =>
7284
{
@@ -93,5 +105,5 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
93105
/// <inheritdoc/>
94106
public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
95107
Action<ITimelineConfigurator> configure)
96-
=> AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
108+
=> AddToggle(triggerObservable, c => c.AddTimeline(configure));
97109
}

src/CodeCasa.AutomationPipelines.Lights/Toggle/CompositeLightTransitionToggleConfigurator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ public ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle()
3434
return this;
3535
}
3636

37+
public ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod)
38+
{
39+
activeConfigurators.Values.ForEach(c => c.SetGracePeriod(gracePeriod));
40+
inactiveConfigurators.Values.ForEach(c => c.SetGracePeriod(gracePeriod));
41+
return this;
42+
}
43+
3744
public ILightTransitionToggleConfigurator<TLight> AddOff()
3845
{
3946
return Add<TurnOffThenPassThroughNode>();

src/CodeCasa.AutomationPipelines.Lights/Toggle/ILightTransitionToggleConfigurator.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ public interface ILightTransitionToggleConfigurator<TLight> where TLight : ILigh
3131
/// <returns>The configurator instance for method chaining.</returns>
3232
ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle();
3333

34+
/// <summary>
35+
/// Sets the grace period during which a manual interaction will use the light's
36+
/// previous state instead of its current state.
37+
/// </summary>
38+
ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod);
39+
3440
/// <summary>
3541
/// Adds an "off" state to the toggle sequence.
3642
/// </summary>

src/CodeCasa.AutomationPipelines.Lights/Toggle/LightTransitionToggleConfigurator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ internal class LightTransitionToggleConfigurator<TLight>(TLight light, ISchedule
1313
public TLight Light { get; } = light;
1414
internal TimeSpan? ToggleTimeout { get; private set; }
1515
internal bool? IncludeOffValue { get; private set; }
16+
internal TimeSpan? GracePeriod { get; private set; }
1617
internal List<Func<IServiceProvider, IPipelineNode<LightTransition>>> NodeFactories
1718
{
1819
get;
@@ -36,6 +37,12 @@ public ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle()
3637
return this;
3738
}
3839

40+
public ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod)
41+
{
42+
GracePeriod = gracePeriod;
43+
return this;
44+
}
45+
3946
public ILightTransitionToggleConfigurator<TLight> AddOff()
4047
{
4148
return Add<TurnOffThenPassThroughNode>();

src/CodeCasa.Lights.NetDaemon/NetDaemonLight.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,10 @@ public ILight[] GetChildren()
7171
public IObservable<Abstractions.StateChange<ILight, LightParameters>> StateChangesWithCurrent() =>
7272
_lightEntity.StateChangesWithCurrent().Select(sc => new Abstractions.StateChange<ILight, LightParameters>(this,
7373
sc.Old?.Attributes?.ToLightParameters(), sc.New?.Attributes?.ToLightParameters()));
74+
75+
/// <inheritdoc />
76+
public DateTime? LastChangedUtc => _lightEntity.EntityState?.LastChanged?.ToUniversalTime();
77+
78+
/// <inheritdoc />
79+
public DateTime? LastUpdatedUtc => _lightEntity.EntityState?.LastUpdated?.ToUniversalTime();
7480
}

0 commit comments

Comments
 (0)