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 @@ -203,11 +203,11 @@ public static ILightTransitionPipelineConfigurator<NetDaemonLight> AddToggle<T>(
IObservable<T> triggerObservable,
params IEntityCore[] sceneEntities)
{
return configurator.AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<NetDaemonLight>>)(c =>
return configurator.AddToggle(triggerObservable, c =>
{
foreach (var scene in sceneEntities)
c.AddScene(scene);
}));
});
}

// -------------------------------------------------------------------------
Expand All @@ -230,11 +230,11 @@ public static ILightTransitionPipelineConfigurator<NetDaemonLight> AddCycle<T>(
IObservable<T> triggerObservable,
params IEntityCore[] sceneEntities)
{
return configurator.AddCycle(triggerObservable, (Action<ILightTransitionCycleConfigurator<NetDaemonLight>>)(c =>
return configurator.AddCycle(triggerObservable, c =>
{
foreach (var scene in sceneEntities)
c.AddScene(scene);
}));
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ public static ILightTransitionReactiveNodeConfigurator<NetDaemonLight> AddToggle
IObservable<T> triggerObservable,
params IEntityCore[] sceneEntities)
{
return configurator.AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<NetDaemonLight>>)(c =>
return configurator.AddToggle(triggerObservable, c =>
{
foreach (var scene in sceneEntities)
c.AddScene(scene);
}));
});
}

// -------------------------------------------------------------------------
Expand All @@ -79,11 +79,11 @@ public static ILightTransitionReactiveNodeConfigurator<NetDaemonLight> AddCycle<
IObservable<T> triggerObservable,
params IEntityCore[] sceneEntities)
{
return configurator.AddCycle(triggerObservable, (Action<ILightTransitionCycleConfigurator<NetDaemonLight>>)(c =>
return configurator.AddCycle(triggerObservable, c =>
{
foreach (var scene in sceneEntities)
c.AddScene(scene);
}));
});
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static IObservable<TValue> ToCycleObservable<TTrigger, TValue>(

public static IObservable<TValue?> ToToggleObservable<TTrigger, TValue>(
this IObservable<TTrigger> triggerObservable,
Func<bool> offCondition,
Func<DateTime?, bool> offCondition,
Func<TValue> offValueFactory,
IEnumerable<Func<TValue>> valueFactories,
TimeSpan timeout,
Expand All @@ -55,19 +55,20 @@ public static IObservable<TValue> ToCycleObservable<TTrigger, TValue>(
{
var utcNow = DateTime.UtcNow;
var consecutive = previousLastChanged != null && utcNow - previousLastChanged < timeout;
previousLastChanged = utcNow;


if (!consecutive)
{
index = 0;
if (offCondition())
if (offCondition(previousLastChanged))
{
previousLastChanged = utcNow;
return offValueFactory();
}
}

var value = index >= valueFactoryArray.Length ? offValueFactory() : valueFactoryArray[index]();
index = index < maxIndexValue ? index + 1 : 0;
previousLastChanged = utcNow;
return value;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,6 @@ public ILightTransitionPipelineConfigurator<TLight> AddToggle<T>(IObservable<T>
public ILightTransitionPipelineConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
Action<ITimelineConfigurator> configure)
{
return AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
return AddToggle(triggerObservable, c => c.AddTimeline(configure));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,37 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
{
var toggleConfigurators = configurators.ToDictionary(kvp => kvp.Key,
kvp => new LightTransitionToggleConfigurator<TLight>(kvp.Value.Light, scheduler));
var compositeCycleConfigurator = new CompositeLightTransitionToggleConfigurator<TLight>(toggleConfigurators, []);
configure(compositeCycleConfigurator);
var compositeToggleConfigurator = new CompositeLightTransitionToggleConfigurator<TLight>(toggleConfigurators, []);
configure(compositeToggleConfigurator);
var shareableTriggerObservable = _observableSharingStrategy.Apply(triggerObservable);
configurators.ForEach(kvp => kvp.Value.AddNodeSource(shareableTriggerObservable.ToToggleObservable(
() => configurators.Values.Any(c => c.Light.IsOn()),
() => new TurnOffThenPassThroughNode(),
toggleConfigurators[kvp.Key].NodeFactories.Select(fact =>
{
return new Func<IPipelineNode<LightTransition>>(() =>
fact.CreateScopedNode(kvp.Value.ServiceProvider) // Note: This service provider already has the light registered. We scope it further for node lifetime.

configurators.ForEach(kvp =>
{
var toggleConfig = toggleConfigurators[kvp.Key];
var gracePeriod = toggleConfig.GracePeriod ?? TimeSpan.FromSeconds(1);
kvp.Value.AddNodeSource(shareableTriggerObservable.ToToggleObservable(
lastActivationTime =>
{
var utcNow = DateTime.UtcNow;
if (utcNow - kvp.Value.Light.LastChangedUtc <= gracePeriod &&
(!lastActivationTime.HasValue || utcNow - lastActivationTime > gracePeriod))
{
return !configurators.Values.Any(c => c.Light.IsOn());
}

return configurators.Values.Any(c => c.Light.IsOn());
},
() => new TurnOffThenPassThroughNode(),
toggleConfig.NodeFactories.Select(fact =>
{
return new Func<IPipelineNode<LightTransition>>(() =>
fact.CreateScopedNode(kvp.Value
.ServiceProvider) // Note: This service provider already has the light registered. We scope it further for node lifetime.
);
}),
toggleConfigurators[kvp.Key].ToggleTimeout ?? TimeSpan.FromMilliseconds(1000),
toggleConfigurators[kvp.Key].IncludeOffValue)));
}),
toggleConfig.ToggleTimeout ?? TimeSpan.FromMilliseconds(1000),
toggleConfig.IncludeOffValue));
});
return this;
}

Expand All @@ -96,5 +113,5 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
/// <inheritdoc/>
public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
Action<ITimelineConfigurator> configure)
=> AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
=> AddToggle(triggerObservable, c => c.AddTimeline(configure));
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,20 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
{
var toggleConfigurator = new LightTransitionToggleConfigurator<TLight>(Light, _scheduler);
configure(toggleConfigurator);

var gracePeriod = toggleConfigurator.GracePeriod ?? TimeSpan.FromSeconds(1);
AddNodeSource(triggerObservable.ToToggleObservable(
() => Light.IsOn(),
lastActivationTime =>
{
var utcNow = DateTime.UtcNow;
if (utcNow - Light.LastChangedUtc <= gracePeriod &&
(!lastActivationTime.HasValue || utcNow - lastActivationTime > gracePeriod))
{
return !Light.IsOn();
}

return Light.IsOn();
},
() => new TurnOffThenPassThroughNode(),
toggleConfigurator.NodeFactories.Select(fact =>
{
Expand All @@ -93,5 +105,5 @@ public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable
/// <inheritdoc/>
public ILightTransitionReactiveNodeConfigurator<TLight> AddToggle<T>(IObservable<T> triggerObservable,
Action<ITimelineConfigurator> configure)
=> AddToggle(triggerObservable, (Action<ILightTransitionToggleConfigurator<TLight>>)(c => c.AddTimeline(configure)));
=> AddToggle(triggerObservable, c => c.AddTimeline(configure));
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ public ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle()
return this;
}

public ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod)
{
activeConfigurators.Values.ForEach(c => c.SetGracePeriod(gracePeriod));
inactiveConfigurators.Values.ForEach(c => c.SetGracePeriod(gracePeriod));
return this;
}

public ILightTransitionToggleConfigurator<TLight> AddOff()
{
return Add<TurnOffThenPassThroughNode>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ public interface ILightTransitionToggleConfigurator<TLight> where TLight : ILigh
/// <returns>The configurator instance for method chaining.</returns>
ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle();

/// <summary>
/// Sets the grace period during which a manual interaction will use the light's
/// previous state instead of its current state.
/// </summary>
ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod);

/// <summary>
/// Adds an "off" state to the toggle sequence.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal class LightTransitionToggleConfigurator<TLight>(TLight light, ISchedule
public TLight Light { get; } = light;
internal TimeSpan? ToggleTimeout { get; private set; }
internal bool? IncludeOffValue { get; private set; }
internal TimeSpan? GracePeriod { get; private set; }
internal List<Func<IServiceProvider, IPipelineNode<LightTransition>>> NodeFactories
{
get;
Expand All @@ -36,6 +37,12 @@ public ILightTransitionToggleConfigurator<TLight> ExcludeOffFromToggleCycle()
return this;
}

public ILightTransitionToggleConfigurator<TLight> SetGracePeriod(TimeSpan gracePeriod)
{
GracePeriod = gracePeriod;
return this;
}

public ILightTransitionToggleConfigurator<TLight> AddOff()
{
return Add<TurnOffThenPassThroughNode>();
Expand Down
6 changes: 6 additions & 0 deletions src/CodeCasa.Lights.NetDaemon/NetDaemonLight.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,10 @@ public ILight[] GetChildren()
public IObservable<Abstractions.StateChange<ILight, LightParameters>> StateChangesWithCurrent() =>
_lightEntity.StateChangesWithCurrent().Select(sc => new Abstractions.StateChange<ILight, LightParameters>(this,
sc.Old?.Attributes?.ToLightParameters(), sc.New?.Attributes?.ToLightParameters()));

/// <inheritdoc />
public DateTime? LastChangedUtc => _lightEntity.EntityState?.LastChanged?.ToUniversalTime();

/// <inheritdoc />
public DateTime? LastUpdatedUtc => _lightEntity.EntityState?.LastUpdated?.ToUniversalTime();
}
24 changes: 24 additions & 0 deletions src/CodeCasa.Lights/ILight.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,28 @@ public interface ILight
/// </summary>
/// <returns>An <see cref="IObservable{T}"/> that emits <see cref="StateChange{ILight, LightParameters}"/> events.</returns>
IObservable<StateChange<ILight, LightParameters>> StateChangesWithCurrent();

/// <summary>
/// Gets the UTC timestamp when the entity's actual state last changed (e.g., from 'off' to 'on').
/// </summary>
/// <value>
/// The <see cref="DateTime"/> in UTC of the last state change, or <see langword="null"/> if unavailable.
/// </value>
/// <remarks>
/// This value only updates when the primary state value changes. It does not update if only
/// entity attributes (like brightness or temperature) change.
/// </remarks>
DateTime? LastChangedUtc { get; }

/// <summary>
/// Gets the UTC timestamp when the entity was last updated.
/// </summary>
/// <value>
/// The <see cref="DateTime"/> in UTC of the last update, or <see langword="null"/> if unavailable.
/// </value>
/// <remarks>
/// This value updates whenever the entity is processed by the system, including when
/// attributes change (e.g., brightness, color) or when a sensor reports the exact same state again.
/// </remarks>
DateTime? LastUpdatedUtc { get; }
}