diff --git a/src/CodeCasa.AutomationPipelines.Lights.Mqtt/CodeCasa.AutomationPipelines.Lights.Mqtt.csproj b/src/CodeCasa.AutomationPipelines.Lights.Mqtt/CodeCasa.AutomationPipelines.Lights.Mqtt.csproj
index b946d9d..2b7752c 100644
--- a/src/CodeCasa.AutomationPipelines.Lights.Mqtt/CodeCasa.AutomationPipelines.Lights.Mqtt.csproj
+++ b/src/CodeCasa.AutomationPipelines.Lights.Mqtt/CodeCasa.AutomationPipelines.Lights.Mqtt.csproj
@@ -34,10 +34,10 @@
-
-
-
-
+
+
+
+
diff --git a/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Groups.cs b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Groups.cs
new file mode 100644
index 0000000..9d0656b
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Groups.cs
@@ -0,0 +1,40 @@
+using CodeCasa.AutomationPipelines.Lights.Pipeline;
+using CodeCasa.Lights.NetDaemon;
+using CodeCasa.Lights.NetDaemon.Extensions;
+using NetDaemon.HassModel.Entities;
+
+namespace CodeCasa.AutomationPipelines.Lights.NetDaemon.Extensions;
+
+///
+/// Extension methods for light pipeline configurators to work with NetDaemon light groups.
+///
+public static partial class LightTransitionPipelineConfiguratorExtensions
+{
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within 10 milliseconds.
+ ///
+ /// The pipeline configurator.
+ /// The NetDaemon light group entity.
+ /// The configurator instance for method chaining.
+ public static ILightTransitionPipelineConfigurator UseLightGroup(
+ this ILightTransitionPipelineConfigurator configurator,
+ ILightEntityCore lightGroupEntity)
+ {
+ return configurator.UseLightGroup(lightGroupEntity.AsLight());
+ }
+
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within the specified time span.
+ ///
+ /// The pipeline configurator.
+ /// The NetDaemon light group entity.
+ /// The time span for the transition.
+ /// The configurator instance for method chaining.
+ public static ILightTransitionPipelineConfigurator UseLightGroup(
+ this ILightTransitionPipelineConfigurator configurator,
+ ILightEntityCore lightGroupEntity,
+ TimeSpan timeSpan)
+ {
+ return configurator.UseLightGroup(lightGroupEntity.AsLight(), timeSpan);
+ }
+}
diff --git a/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Scene.cs b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Scene.cs
index b4a5c61..f071f64 100644
--- a/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Scene.cs
+++ b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionPipelineConfiguratorExtensions.Scene.cs
@@ -1,6 +1,4 @@
-using CodeCasa.AutomationPipelines.Lights.Cycle;
using CodeCasa.AutomationPipelines.Lights.Pipeline;
-using CodeCasa.AutomationPipelines.Lights.Toggle;
using CodeCasa.Lights.NetDaemon;
using CodeCasa.Lights.NetDaemon.Scenes;
using NetDaemon.HassModel.Entities;
diff --git a/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.Scene.cs b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.Scene.cs
index c828cf9..b52f403 100644
--- a/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.Scene.cs
+++ b/src/CodeCasa.AutomationPipelines.Lights.NetDaemon/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.Scene.cs
@@ -1,6 +1,4 @@
-using CodeCasa.AutomationPipelines.Lights.Cycle;
using CodeCasa.AutomationPipelines.Lights.ReactiveNode;
-using CodeCasa.AutomationPipelines.Lights.Toggle;
using CodeCasa.Lights.NetDaemon;
using CodeCasa.Lights.NetDaemon.Scenes;
using NetDaemon.HassModel.Entities;
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNode.cs b/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNode.cs
new file mode 100644
index 0000000..89c59e3
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNode.cs
@@ -0,0 +1,32 @@
+using CodeCasa.Lights;
+
+namespace CodeCasa.AutomationPipelines.Lights.Nodes;
+
+internal class GroupNode : PipelineNode
+{
+ private readonly GroupNodeContext _groupNodeContext;
+ private readonly ILight _light;
+
+ public GroupNode(ILight light, GroupNodeContext groupNodeContext)
+ {
+ _light = light;
+ _groupNodeContext = groupNodeContext;
+ Name = "Group Node";
+ }
+
+ ///
+ protected override void InputReceived(LightTransition? input)
+ {
+ if (input != null)
+ {
+ _groupNodeContext.Process(_light, input);
+ }
+ }
+
+ ///
+ public override async ValueTask DisposeAsync()
+ {
+ _groupNodeContext.Unregister(_light);
+ await base.DisposeAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNodeContext.cs b/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNodeContext.cs
new file mode 100644
index 0000000..48d8447
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Nodes/GroupNodeContext.cs
@@ -0,0 +1,220 @@
+using System.Reactive.Concurrency;
+using CodeCasa.Lights;
+using Microsoft.Extensions.Logging;
+
+namespace CodeCasa.AutomationPipelines.Lights.Nodes
+{
+ internal class GroupNodeContext(IScheduler scheduler, ILogger>? logger)
+ {
+ private readonly List _groups = new();
+ private readonly Lock _lock = new();
+
+ public void Register(ILight light, ILight lightGroup, TimeSpan groupDuration, EqualityComparer equalityComparer)
+ {
+ lock (_lock)
+ {
+ var existingGroup = _groups.FirstOrDefault(g => g.LightGroup == lightGroup);
+ if (existingGroup == null)
+ {
+ existingGroup = new GroupInfo(lightGroup, light, equalityComparer, groupDuration, scheduler, logger);
+ _groups.Add(existingGroup);
+ }
+ else
+ {
+ existingGroup.AddMember(light);
+ }
+ }
+ }
+
+ public void Process(ILight light, LightTransition transition)
+ {
+ var inputInfo = new InputInfo(DateTime.UtcNow, light, transition);
+ lock (_lock)
+ {
+ foreach (var group in _groups)
+ {
+ group.Process(inputInfo);
+ }
+ }
+
+ }
+
+ public void Unregister(ILight light)
+ {
+ lock (_lock)
+ {
+ foreach (var group in _groups.ToArray())
+ {
+ if (group.RemoveMember(light))
+ {
+ _groups.Remove(group);
+ group.Dispose();
+ }
+ }
+ }
+ }
+
+ internal class InputInfo(DateTime timestamp, ILight light, LightTransition lightTransition)
+ {
+ public LightTransition Transition { get; } = lightTransition;
+ public ILight Light { get; } = light;
+ public DateTime Timestamp { get; } = timestamp;
+ public bool HasExecuted { get; private set; }
+ public void Execute()
+ {
+ if (HasExecuted)
+ {
+ return;
+ }
+ Light.ApplyTransition(Transition);
+ HasExecuted = true;
+ }
+ }
+
+ internal class GroupInfo(
+ ILight lightGroup,
+ ILight firstGroupMember,
+ IEqualityComparer equalityComparer,
+ TimeSpan groupDuration,
+ IScheduler scheduler,
+ ILogger>? logger)
+ : IDisposable
+ {
+ public ILight LightGroup { get; } = lightGroup;
+ private readonly List _groupMembers = [firstGroupMember];
+ private readonly Dictionary _groupInputs = new();
+ private readonly Dictionary _scheduledWork = new();
+ private readonly Lock _lock = new();
+
+ public void AddMember(ILight member)
+ {
+ lock (_lock)
+ {
+ _groupMembers.Add(member);
+ }
+ }
+
+ public bool RemoveMember(ILight member)
+ {
+ lock (_lock)
+ {
+ _groupMembers.Remove(member);
+ _groupInputs.Remove(member);
+ CleanupScheduledWork(member);
+
+ return !_groupMembers.Any();
+ }
+ }
+
+ public void Process(InputInfo inputInfo)
+ {
+ lock (_lock)
+ {
+ if (!_groupMembers.Contains(inputInfo.Light))
+ {
+ return;
+ }
+
+ // Clean up expired or executed inputs
+ CleanupExpiredInputs(inputInfo.Timestamp);
+
+ // If there's an existing input for this light, execute it first
+ if (_groupInputs.TryGetValue(inputInfo.Light, out var existingInput))
+ {
+ existingInput.Execute();
+ CleanupScheduledWork(inputInfo.Light);
+ }
+
+ // Add the new input
+ _groupInputs[inputInfo.Light] = inputInfo;
+
+ // Check if all group members now have matching transitions
+ if (AllMembersHaveMatchingTransitions(inputInfo.Transition))
+ {
+ // All members are in sync - apply to the group instead
+ _groupInputs.Clear();
+ CleanupAllScheduledWork();
+ logger?.LogInformation($"Group [{LightGroup.Id}] used. All members have matching transition: {inputInfo.Transition}");
+ LightGroup.ApplyTransition(inputInfo.Transition);
+ return;
+ }
+
+ // Schedule this input for individual execution if no group consensus is reached
+ var scheduledWork = scheduler.Schedule(groupDuration, () =>
+ {
+ lock (_lock)
+ {
+ if (_groupInputs.TryGetValue(inputInfo.Light, out var info) && !info.HasExecuted)
+ {
+ info.Execute();
+ _groupInputs.Remove(inputInfo.Light);
+ }
+ _scheduledWork.Remove(inputInfo.Light);
+ }
+ });
+ _scheduledWork[inputInfo.Light] = scheduledWork;
+ }
+ }
+
+ private void CleanupExpiredInputs(DateTime currentTime)
+ {
+ foreach (var kvp in _groupInputs.ToArray())
+ {
+ var info = kvp.Value;
+ if (info.HasExecuted)
+ {
+ // This can occur if the light is in multiple groups at once.
+ _groupInputs.Remove(info.Light);
+ CleanupScheduledWork(info.Light);
+ }
+ else if (info.Timestamp + groupDuration < currentTime)
+ {
+ // We waited long enough for this light to be part of the group,
+ // but it never received a transition that matched the other lights in the group.
+ info.Execute();
+ _groupInputs.Remove(info.Light);
+ CleanupScheduledWork(info.Light);
+ }
+ }
+ }
+
+ private bool AllMembersHaveMatchingTransitions(LightTransition transition)
+ {
+ // We need inputs from ALL group members
+ if (_groupInputs.Count != _groupMembers.Count)
+ {
+ return false;
+ }
+
+ // All inputs must have matching transitions
+ return _groupInputs.Values.All(info => equalityComparer.Equals(info.Transition, transition));
+ }
+
+ private void CleanupScheduledWork(ILight light)
+ {
+ if (_scheduledWork.TryGetValue(light, out var disposable))
+ {
+ disposable.Dispose();
+ _scheduledWork.Remove(light);
+ }
+ }
+
+ private void CleanupAllScheduledWork()
+ {
+ foreach (var disposable in _scheduledWork.Values)
+ {
+ disposable.Dispose();
+ }
+ _scheduledWork.Clear();
+ }
+
+ public void Dispose()
+ {
+ lock (_lock)
+ {
+ CleanupAllScheduledWork();
+ }
+ }
+ }
+ }
+}
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/CompositeLightTransitionPipelineConfigurator.Groups.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/CompositeLightTransitionPipelineConfigurator.Groups.cs
new file mode 100644
index 0000000..9107efa
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/CompositeLightTransitionPipelineConfigurator.Groups.cs
@@ -0,0 +1,31 @@
+using CodeCasa.AutomationPipelines.Lights.Extensions;
+using CodeCasa.Lights;
+
+namespace CodeCasa.AutomationPipelines.Lights.Pipeline;
+
+internal partial class CompositeLightTransitionPipelineConfigurator
+{
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup)
+ {
+ NodeContainers.Values.ForEach(b => b.UseLightGroup(lightGroup));
+ return this;
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, EqualityComparer comparer)
+ {
+ NodeContainers.Values.ForEach(b => b.UseLightGroup(lightGroup, comparer));
+ return this;
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan)
+ {
+ NodeContainers.Values.ForEach(b => b.UseLightGroup(lightGroup, timeSpan));
+ return this;
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan, EqualityComparer comparer)
+ {
+ NodeContainers.Values.ForEach(b => b.UseLightGroup(lightGroup, timeSpan, comparer));
+ return this;
+ }
+}
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/ILightTransitionPipelineConfigurator.Groups.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/ILightTransitionPipelineConfigurator.Groups.cs
new file mode 100644
index 0000000..68c5d1e
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/ILightTransitionPipelineConfigurator.Groups.cs
@@ -0,0 +1,38 @@
+using CodeCasa.Lights;
+
+namespace CodeCasa.AutomationPipelines.Lights.Pipeline;
+
+public partial interface ILightTransitionPipelineConfigurator
+{
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within 20 milliseconds.
+ ///
+ /// The light group entity.
+ /// The configurator instance for method chaining.
+ ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup);
+
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within 20 milliseconds, using a custom comparer.
+ ///
+ /// The light group entity.
+ /// The equality comparer to determine if transitions are the same.
+ /// The configurator instance for method chaining.
+ ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, EqualityComparer comparer);
+
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within the specified time span.
+ ///
+ /// The light group entity.
+ /// The time span for the transition.
+ /// The configurator instance for method chaining.
+ ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan);
+
+ ///
+ /// Allows you to provide a group light entity to be used if the same transition is applied to all lights at once within the specified time span, using a custom comparer.
+ ///
+ /// The light group entity.
+ /// The time span for the transition.
+ /// The equality comparer to determine if transitions are the same.
+ /// The configurator instance for method chaining.
+ ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan, EqualityComparer comparer);
+}
\ No newline at end of file
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightGroupConfig.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightGroupConfig.cs
new file mode 100644
index 0000000..21e9f49
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightGroupConfig.cs
@@ -0,0 +1,5 @@
+using CodeCasa.Lights;
+
+namespace CodeCasa.AutomationPipelines.Lights.Pipeline;
+
+internal record LightGroupConfig(TimeSpan TimeSpan, EqualityComparer Comparer);
\ No newline at end of file
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightPipelineFactory.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightPipelineFactory.cs
index 1f72cb7..55a56cb 100644
--- a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightPipelineFactory.cs
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightPipelineFactory.cs
@@ -117,14 +117,28 @@ internal Dictionary> CreateLightPipelines(), logger);
+
return configurators.ToDictionary(kvp => kvp.Key, kvp =>
{
var conf = kvp.Value;
+ var nodes = conf.Nodes.ToList();
+
+ if (conf.LightGroups.Any())
+ {
+ var groupNode = new GroupNode(conf.Light, groupContext);
+ foreach (var lightGroup in conf.LightGroups)
+ {
+ groupContext.Register(conf.Light, lightGroup.Key, lightGroup.Value.TimeSpan, lightGroup.Value.Comparer);
+ }
+ nodes.Add(groupNode);
+ }
+
IPipeline pipeline = new Pipeline(
LightTransition.Off(),
- conf.Nodes,
+ nodes,
conf.Light.ApplyTransition,
- conf.EqualityComparer)
+ conf.DistinctEqualityComparer)
{
Name = conf.Name
};
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.Groups.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.Groups.cs
new file mode 100644
index 0000000..2a0fbcc
--- /dev/null
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.Groups.cs
@@ -0,0 +1,29 @@
+using CodeCasa.Lights;
+
+namespace CodeCasa.AutomationPipelines.Lights.Pipeline;
+
+internal partial class LightTransitionPipelineConfigurator
+{
+ public Dictionary LightGroups = new();
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup)
+ {
+ return UseLightGroup(lightGroup, TimeSpan.FromMilliseconds(20));
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, EqualityComparer comparer)
+ {
+ return UseLightGroup(lightGroup, TimeSpan.FromMilliseconds(20), comparer);
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan)
+ {
+ return UseLightGroup(lightGroup, timeSpan, EqualityComparer.Default);
+ }
+
+ public ILightTransitionPipelineConfigurator UseLightGroup(ILight lightGroup, TimeSpan timeSpan, EqualityComparer comparer)
+ {
+ LightGroups[lightGroup] = new LightGroupConfig(timeSpan, comparer);
+ return this;
+ }
+}
diff --git a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.cs b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.cs
index 70cbb4c..14f8b1a 100644
--- a/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.cs
+++ b/src/CodeCasa.AutomationPipelines.Lights/Pipeline/LightTransitionPipelineConfigurator.cs
@@ -19,7 +19,7 @@ internal partial class LightTransitionPipelineConfigurator
internal string? Name { get; set; } = "Pipeline";
internal TLight Light { get; }
- internal IEqualityComparer? EqualityComparer { get; private set; }
+ internal IEqualityComparer? DistinctEqualityComparer { get; private set; }
public LightTransitionPipelineConfigurator(IServiceProvider serviceProvider, TLight light)
{
@@ -50,7 +50,7 @@ public ILightTransitionPipelineConfigurator WithDistinctOutput()
///
public ILightTransitionPipelineConfigurator WithDistinctOutput(IEqualityComparer equalityComparer)
{
- EqualityComparer = equalityComparer;
+ DistinctEqualityComparer = equalityComparer;
return this;
}
diff --git a/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj b/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj
index f6fa62f..b6d33cd 100644
--- a/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj
+++ b/src/CodeCasa.AutomationPipelines/CodeCasa.AutomationPipelines.csproj
@@ -34,9 +34,9 @@
-
-
-
+
+
+
diff --git a/src/CodeCasa.Lights.NetDaemon.Scenes/CodeCasa.Lights.NetDaemon.Scenes.csproj b/src/CodeCasa.Lights.NetDaemon.Scenes/CodeCasa.Lights.NetDaemon.Scenes.csproj
index 1f86848..1ed9431 100644
--- a/src/CodeCasa.Lights.NetDaemon.Scenes/CodeCasa.Lights.NetDaemon.Scenes.csproj
+++ b/src/CodeCasa.Lights.NetDaemon.Scenes/CodeCasa.Lights.NetDaemon.Scenes.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/CodeCasa.Lights.NetDaemon/CodeCasa.Lights.NetDaemon.csproj b/src/CodeCasa.Lights.NetDaemon/CodeCasa.Lights.NetDaemon.csproj
index 9512166..fda8678 100644
--- a/src/CodeCasa.Lights.NetDaemon/CodeCasa.Lights.NetDaemon.csproj
+++ b/src/CodeCasa.Lights.NetDaemon/CodeCasa.Lights.NetDaemon.csproj
@@ -39,7 +39,7 @@
-
+
diff --git a/src/CodeCasa.Lights.Timelines/CodeCasa.Lights.Timelines.csproj b/src/CodeCasa.Lights.Timelines/CodeCasa.Lights.Timelines.csproj
index dbc7f38..90c83be 100644
--- a/src/CodeCasa.Lights.Timelines/CodeCasa.Lights.Timelines.csproj
+++ b/src/CodeCasa.Lights.Timelines/CodeCasa.Lights.Timelines.csproj
@@ -37,7 +37,7 @@
-
+
diff --git a/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj b/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj
index c2bf868..c8a0731 100644
--- a/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj
+++ b/src/CodeCasa.NetDaemon.Extensions.Observables/CodeCasa.NetDaemon.Extensions.Observables.csproj
@@ -34,9 +34,9 @@
-
+
-
+
diff --git a/src/CodeCasa.NetDaemon.RuntimeState/CodeCasa.NetDaemon.RuntimeState.csproj b/src/CodeCasa.NetDaemon.RuntimeState/CodeCasa.NetDaemon.RuntimeState.csproj
index a2963f2..3aac239 100644
--- a/src/CodeCasa.NetDaemon.RuntimeState/CodeCasa.NetDaemon.RuntimeState.csproj
+++ b/src/CodeCasa.NetDaemon.RuntimeState/CodeCasa.NetDaemon.RuntimeState.csproj
@@ -34,9 +34,9 @@
-
-
-
+
+
+
diff --git a/src/CodeCasa.NetDaemon.Sensors.Composite/CodeCasa.NetDaemon.Sensors.Composite.csproj b/src/CodeCasa.NetDaemon.Sensors.Composite/CodeCasa.NetDaemon.Sensors.Composite.csproj
index eac5565..f3bdea8 100644
--- a/src/CodeCasa.NetDaemon.Sensors.Composite/CodeCasa.NetDaemon.Sensors.Composite.csproj
+++ b/src/CodeCasa.NetDaemon.Sensors.Composite/CodeCasa.NetDaemon.Sensors.Composite.csproj
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/src/CodeCasa.NetDaemon.TypedEntities/CodeCasa.NetDaemon.TypedEntities.csproj b/src/CodeCasa.NetDaemon.TypedEntities/CodeCasa.NetDaemon.TypedEntities.csproj
index fdf31ed..2da2cd9 100644
--- a/src/CodeCasa.NetDaemon.TypedEntities/CodeCasa.NetDaemon.TypedEntities.csproj
+++ b/src/CodeCasa.NetDaemon.TypedEntities/CodeCasa.NetDaemon.TypedEntities.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/CodeCasa.Notifications.InputSelect.NetDaemon/CodeCasa.Notifications.InputSelect.NetDaemon.csproj b/src/CodeCasa.Notifications.InputSelect.NetDaemon/CodeCasa.Notifications.InputSelect.NetDaemon.csproj
index 94036fb..cafe41d 100644
--- a/src/CodeCasa.Notifications.InputSelect.NetDaemon/CodeCasa.Notifications.InputSelect.NetDaemon.csproj
+++ b/src/CodeCasa.Notifications.InputSelect.NetDaemon/CodeCasa.Notifications.InputSelect.NetDaemon.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/CodeCasa.Notifications.Phone.NetDaemon/CodeCasa.Notifications.Phone.NetDaemon.csproj b/src/CodeCasa.Notifications.Phone.NetDaemon/CodeCasa.Notifications.Phone.NetDaemon.csproj
index 6b50fbf..4fea7e6 100644
--- a/src/CodeCasa.Notifications.Phone.NetDaemon/CodeCasa.Notifications.Phone.NetDaemon.csproj
+++ b/src/CodeCasa.Notifications.Phone.NetDaemon/CodeCasa.Notifications.Phone.NetDaemon.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj b/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj
index b47233a..62167cb 100644
--- a/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj
+++ b/tests/CodeCasa.AutomationPipelines.Lights.Tests/CodeCasa.AutomationPipelines.Lights.Tests.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/tests/CodeCasa.AutomationPipelines.Tests/CodeCasa.AutomationPipelines.Tests.csproj b/tests/CodeCasa.AutomationPipelines.Tests/CodeCasa.AutomationPipelines.Tests.csproj
index f19ce7d..487fd99 100644
--- a/tests/CodeCasa.AutomationPipelines.Tests/CodeCasa.AutomationPipelines.Tests.csproj
+++ b/tests/CodeCasa.AutomationPipelines.Tests/CodeCasa.AutomationPipelines.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/CodeCasa.Lights.Tests/CodeCasa.Lights.Tests.csproj b/tests/CodeCasa.Lights.Tests/CodeCasa.Lights.Tests.csproj
index 86a3b71..f5504c7 100644
--- a/tests/CodeCasa.Lights.Tests/CodeCasa.Lights.Tests.csproj
+++ b/tests/CodeCasa.Lights.Tests/CodeCasa.Lights.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/CodeCasa.NetDaemon.Extensions.Observables.Tests/CodeCasa.NetDaemon.Extensions.Observables.Tests.csproj b/tests/CodeCasa.NetDaemon.Extensions.Observables.Tests/CodeCasa.NetDaemon.Extensions.Observables.Tests.csproj
index 4bd3981..45360d1 100644
--- a/tests/CodeCasa.NetDaemon.Extensions.Observables.Tests/CodeCasa.NetDaemon.Extensions.Observables.Tests.csproj
+++ b/tests/CodeCasa.NetDaemon.Extensions.Observables.Tests/CodeCasa.NetDaemon.Extensions.Observables.Tests.csproj
@@ -8,10 +8,10 @@
-
+
-
-
+
+
diff --git a/tests/CodeCasa.NetDaemon.Sensors.Composite.Tests/CodeCasa.NetDaemon.Sensors.Composite.Tests.csproj b/tests/CodeCasa.NetDaemon.Sensors.Composite.Tests/CodeCasa.NetDaemon.Sensors.Composite.Tests.csproj
index bb258ac..f6785e3 100644
--- a/tests/CodeCasa.NetDaemon.Sensors.Composite.Tests/CodeCasa.NetDaemon.Sensors.Composite.Tests.csproj
+++ b/tests/CodeCasa.NetDaemon.Sensors.Composite.Tests/CodeCasa.NetDaemon.Sensors.Composite.Tests.csproj
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests.csproj b/tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests.csproj
index 22aff5f..dd257ab 100644
--- a/tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests.csproj
+++ b/tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests/CodeCasa.Notifications.InputSelect.NetDaemon.Tests.csproj
@@ -8,7 +8,7 @@
-
+