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 @@ - +