diff --git a/CodeCasa.sln b/CodeCasa.sln
index 17aa403..d9267bf 100644
--- a/CodeCasa.sln
+++ b/CodeCasa.sln
@@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.Lights.NetDaemon.S
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.AutomationPipelines.Lights.Tests", "tests\CodeCasa.AutomationPipelines.Lights.Tests\CodeCasa.AutomationPipelines.Lights.Tests.csproj", "{96DB93C1-036A-436A-AF7A-AEC07243A929}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeCasa.Notifications.Lights", "src\CodeCasa.Notifications.Lights\CodeCasa.Notifications.Lights.csproj", "{B79CCDE0-2EB5-453B-B42C-B52014D8F3D6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -113,6 +115,10 @@ Global
{96DB93C1-036A-436A-AF7A-AEC07243A929}.Debug|Any CPU.Build.0 = Debug|Any CPU
{96DB93C1-036A-436A-AF7A-AEC07243A929}.Release|Any CPU.ActiveCfg = Release|Any CPU
{96DB93C1-036A-436A-AF7A-AEC07243A929}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B79CCDE0-2EB5-453B-B42C-B52014D8F3D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B79CCDE0-2EB5-453B-B42C-B52014D8F3D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B79CCDE0-2EB5-453B-B42C-B52014D8F3D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B79CCDE0-2EB5-453B-B42C-B52014D8F3D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj b/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj
index 1e78424..c18f3ad 100644
--- a/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj
+++ b/src/CodeCasa.AutomationPipelines.Lights/CodeCasa.AutomationPipelines.Lights.csproj
@@ -19,6 +19,7 @@
+
diff --git a/src/CodeCasa.Notifications.Lights/CodeCasa.Notifications.Lights.csproj b/src/CodeCasa.Notifications.Lights/CodeCasa.Notifications.Lights.csproj
new file mode 100644
index 0000000..61d96c3
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/CodeCasa.Notifications.Lights.csproj
@@ -0,0 +1,40 @@
+
+
+
+ net10.0
+ enable
+ enable
+ CodeCasa.Notifications.Lights
+ Jasper Lammers
+ DevJasper
+ Light notification system for CodeCasa, enabling visual alerts and notifications using smart lights.
+ https://github.com/DevJasperNL/CodeCasa
+ Home Automation;Home Assistant;HA Integration;Smart Home;Automation;Lights;Light;Notification;Notifications;Alert;Alerts;C#;.NET;DotNet
+ LICENSE
+ README.md
+ CodeCasa.Notifications.Lights
+ True
+ cc_icon.png
+ https://github.com/DevJasperNL/CodeCasa/releases
+
+
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+ True
+ \
+
+
+
+
+
+
+
+
diff --git a/src/CodeCasa.Notifications.Lights/Config/LightNotificationConfig.cs b/src/CodeCasa.Notifications.Lights/Config/LightNotificationConfig.cs
new file mode 100644
index 0000000..211d786
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/Config/LightNotificationConfig.cs
@@ -0,0 +1,39 @@
+using CodeCasa.AutomationPipelines;
+using CodeCasa.AutomationPipelines.Lights.Context;
+using CodeCasa.Lights;
+
+namespace CodeCasa.Notifications.Lights.Config;
+
+///
+/// Configuration for a light notification.
+///
+public class LightNotificationConfig
+{
+ ///
+ /// Gets or sets the priority of the notification.
+ /// Higher values indicate higher priority.
+ ///
+ public int Priority { get; set; }
+
+ ///
+ /// Gets the type of the pipeline node associated with the notification.
+ ///
+ public Type? NodeType { get; }
+
+ ///
+ /// Gets the factory function to create the pipeline node associated with the notification.
+ ///
+ public Func>? NodeFactory { get; }
+
+ internal LightNotificationConfig(Type nodeType, int priority)
+ {
+ NodeType = nodeType;
+ Priority = priority;
+ }
+
+ internal LightNotificationConfig(Func> nodeFactory, int priority)
+ {
+ NodeFactory = nodeFactory;
+ Priority = priority;
+ }
+}
\ No newline at end of file
diff --git a/src/CodeCasa.Notifications.Lights/Extensions/LightTransitionPipelineConfiguratorExtensions.cs b/src/CodeCasa.Notifications.Lights/Extensions/LightTransitionPipelineConfiguratorExtensions.cs
new file mode 100644
index 0000000..b0ecd69
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/Extensions/LightTransitionPipelineConfiguratorExtensions.cs
@@ -0,0 +1,48 @@
+using CodeCasa.AutomationPipelines;
+using CodeCasa.AutomationPipelines.Lights.Context;
+using CodeCasa.AutomationPipelines.Lights.Extensions;
+using CodeCasa.AutomationPipelines.Lights.Pipeline;
+using CodeCasa.Lights;
+using System.Reactive.Linq;
+
+namespace CodeCasa.Notifications.Lights.Extensions
+{
+ ///
+ /// Extension methods for configuring light transition pipelines with notifications.
+ ///
+ public static class LightTransitionPipelineConfiguratorExtensions
+ {
+ ///
+ /// Adds light notifications to the pipeline.
+ ///
+ /// The pipeline configurator.
+ /// The context providing light notifications.
+ /// The updated pipeline configurator.
+ public static ILightTransitionPipelineConfigurator AddNotifications(
+ this ILightTransitionPipelineConfigurator configurator, LightNotificationManagerContext lightNotificationManagerContext)
+ {
+ return configurator.AddReactiveNode(c =>
+ {
+ c.AddNodeSource(lightNotificationManagerContext.LightNotifications.Select(lnc =>
+ {
+ if (lnc == null)
+ {
+ return new Func?>(_ => null);
+ }
+
+ if (lnc.NodeFactory != null)
+ {
+ return lnc.NodeFactory;
+ }
+
+ if (lnc.NodeType == null)
+ {
+ throw new InvalidOperationException("Both NodeFactory and NodeType are null.");
+ }
+
+ return ctx => (IPipelineNode)ctx.ServiceProvider.CreateInstanceWithinContext(lnc.NodeType, ctx);
+ }));
+ });
+ }
+ }
+}
diff --git a/src/CodeCasa.Notifications.Lights/Extensions/ServiceCollectionExtensions.cs b/src/CodeCasa.Notifications.Lights/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..3b8bd1e
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,22 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace CodeCasa.Notifications.Lights.Extensions;
+
+///
+/// Extension methods for setting up light notification services in an .
+///
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Adds light notification services to the specified .
+ ///
+ /// The to add services to.
+ /// The so that additional calls can be chained.
+ public static IServiceCollection AddLightNotifications(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddSingleton()
+ .AddTransient()
+ .AddTransient();
+ }
+}
\ No newline at end of file
diff --git a/src/CodeCasa.Notifications.Lights/LightNotification.cs b/src/CodeCasa.Notifications.Lights/LightNotification.cs
new file mode 100644
index 0000000..6100f94
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/LightNotification.cs
@@ -0,0 +1,19 @@
+namespace CodeCasa.Notifications.Lights
+{
+ ///
+ /// Represents a light notification.
+ ///
+ /// The unique identifier for the notification.
+ /// The disposable object associated with the notification.
+ public class LightNotification(string id, IDisposable disposable) : IDisposable
+ {
+ ///
+ /// Gets the unique identifier for the notification.
+ /// This ID can be used to update or cancel the notification later.
+ ///
+ public string Id { get; } = id;
+
+ ///
+ public void Dispose() => disposable.Dispose();
+ }
+}
diff --git a/src/CodeCasa.Notifications.Lights/LightNotificationContext.cs b/src/CodeCasa.Notifications.Lights/LightNotificationContext.cs
new file mode 100644
index 0000000..1a55d60
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/LightNotificationContext.cs
@@ -0,0 +1,113 @@
+using CodeCasa.AutomationPipelines;
+using CodeCasa.AutomationPipelines.Lights.Context;
+using CodeCasa.Lights;
+using CodeCasa.Notifications.Lights.Config;
+
+namespace CodeCasa.Notifications.Lights
+{
+ ///
+ /// Context for managing light notifications.
+ ///
+ /// The manager responsible for handling light notifications.
+ public class LightNotificationContext(LightNotificationManager lightNotificationManager)
+ {
+ ///
+ /// Notifies with a new light notification, replacing an existing one.
+ ///
+ /// The type of the pipeline node representing the notification behavior.
+ /// The existing light notification to replace.
+ /// The created or updated light notification.
+ public LightNotification Notify(LightNotification lightNotificationToReplace) where TNode : IPipelineNode => Notify(lightNotificationToReplace, 0);
+
+ ///
+ /// Notifies with a new light notification with a specific priority, replacing an existing one.
+ ///
+ /// The type of the pipeline node representing the notification behavior.
+ /// The existing light notification to replace.
+ /// The priority of the notification.
+ /// The created or updated light notification.
+ public LightNotification Notify(LightNotification lightNotificationToReplace, int priority) where TNode : IPipelineNode
+ {
+ return Notify(lightNotificationToReplace.Id, priority);
+ }
+
+ ///
+ /// Notifies with a new light notification using a specific ID.
+ ///
+ /// The type of the pipeline node representing the notification behavior.
+ /// The unique identifier for the notification.
+ /// The created or updated light notification.
+ public LightNotification Notify(string notificationId) where TNode : IPipelineNode => Notify(notificationId, 0);
+
+ ///
+ /// Notifies with a new light notification with a specific priority and ID.
+ ///
+ /// The type of the pipeline node representing the notification behavior.
+ /// The unique identifier for the notification.
+ /// The priority of the notification.
+ /// The created or updated light notification.
+ public LightNotification Notify(string notificationId, int priority) where TNode : IPipelineNode
+ {
+ return lightNotificationManager.Notify(new LightNotificationConfig(typeof(TNode), priority), notificationId);
+ }
+
+ ///
+ /// Notifies with a new light notification using a factory, replacing an existing one.
+ ///
+ /// The existing light notification to replace.
+ /// The factory function to create the pipeline node.
+ /// The created or updated light notification.
+ public LightNotification Notify(LightNotification lightNotificationToReplace, Func> nodeFactory) => Notify(lightNotificationToReplace, nodeFactory, 0);
+
+ ///
+ /// Notifies with a new light notification using a factory and specific priority, replacing an existing one.
+ ///
+ /// The existing light notification to replace.
+ /// The factory function to create the pipeline node.
+ /// The priority of the notification.
+ /// The created or updated light notification.
+ public LightNotification Notify(LightNotification lightNotificationToReplace, Func> nodeFactory, int priority)
+ {
+ return Notify(lightNotificationToReplace.Id, nodeFactory, priority);
+ }
+
+ ///
+ /// Notifies with a new light notification using a factory and specific ID.
+ ///
+ /// The unique identifier for the notification.
+ /// The factory function to create the pipeline node.
+ /// The created or updated light notification.
+ public LightNotification Notify(string notificationId, Func> nodeFactory) => Notify(notificationId, nodeFactory, 0);
+
+ ///
+ /// Notifies with a new light notification using a factory, specific priority, and ID.
+ ///
+ /// The unique identifier for the notification.
+ /// The factory function to create the pipeline node.
+ /// The priority of the notification.
+ /// The created or updated light notification.
+ public LightNotification Notify(string notificationId, Func> nodeFactory, int priority)
+ {
+ return lightNotificationManager.Notify(new LightNotificationConfig(nodeFactory, priority), notificationId);
+ }
+
+ ///
+ /// Removes a specific light notification.
+ ///
+ /// The light notification to remove.
+ public void RemoveNotification(LightNotification notificationToRemove)
+ {
+ RemoveNotification(notificationToRemove.Id);
+ }
+
+ ///
+ /// Removes a light notification by its ID.
+ ///
+ /// The unique identifier of the notification to remove.
+ /// True if the notification was successfully removed; otherwise, false.
+ public bool RemoveNotification(string id)
+ {
+ return lightNotificationManager.Remove(id);
+ }
+ }
+}
diff --git a/src/CodeCasa.Notifications.Lights/LightNotificationManager.cs b/src/CodeCasa.Notifications.Lights/LightNotificationManager.cs
new file mode 100644
index 0000000..7dd3da2
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/LightNotificationManager.cs
@@ -0,0 +1,92 @@
+using System.Reactive.Disposables;
+using System.Reactive.Subjects;
+using CodeCasa.Notifications.Lights.Config;
+
+namespace CodeCasa.Notifications.Lights
+{
+ ///
+ /// Manages light notifications, handling their priorities and active states.
+ ///
+ public class LightNotificationManager
+ {
+ private readonly Lock _lock = new();
+ private readonly BehaviorSubject _subject = new(null);
+
+ private readonly Dictionary _activeNotifications = new();
+
+ ///
+ /// Gets an observable sequence of the current active light notification configuration.
+ ///
+ /// An observable that emits the current light notification configuration or null if none are active.
+ public IObservable LightNotifications()
+ {
+ lock (_lock)
+ {
+ return _subject;
+ }
+ }
+
+ ///
+ /// Notifies with a new light notification configuration, replacing an existing notification.
+ ///
+ /// The configuration for the new notification.
+ /// The existing notification to replace.
+ /// The created light notification.
+ public LightNotification Notify(LightNotificationConfig lightNotificationOptions, LightNotification lightNotificationToReplace)
+ {
+ return Notify(lightNotificationOptions, lightNotificationToReplace.Id);
+ }
+
+ ///
+ /// Notifies with a new light notification configuration using a specific ID.
+ ///
+ /// The configuration for the new notification.
+ /// The unique identifier for the notification.
+ /// The created light notification.
+ public LightNotification Notify(LightNotificationConfig lightNotificationOptions, string id)
+ {
+ lock (_lock)
+ {
+ var highestPrio = _activeNotifications.Any() ? (int?)_activeNotifications.Values.Max(n => n.Priority) : null;
+ if (highestPrio == null || lightNotificationOptions.Priority >= highestPrio)
+ {
+ _subject.OnNext(lightNotificationOptions);
+ }
+
+ _activeNotifications[id] = lightNotificationOptions;
+ return new LightNotification(id, Disposable.Create(() => Remove(id)));
+ }
+ }
+
+ ///
+ /// Removes a light notification by its ID.
+ ///
+ /// The unique identifier of the notification to remove.
+ /// True if the notification was successfully removed; otherwise, false.
+ public bool Remove(string id)
+ {
+ lock (_lock)
+ {
+ if (!_activeNotifications.Remove(id, out var configAndDisposable))
+ {
+ return false;
+ }
+
+ if (!_activeNotifications.Any())
+ {
+ _subject.OnNext(null);
+ return true;
+ }
+
+ var highestKvp = _activeNotifications.MaxBy(kvp => kvp.Value.Priority);
+ if (configAndDisposable.Priority < highestKvp.Value.Priority)
+ {
+ return true;
+ }
+
+ _subject.OnNext(highestKvp.Value);
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/CodeCasa.Notifications.Lights/LightNotificationManagerContext.cs b/src/CodeCasa.Notifications.Lights/LightNotificationManagerContext.cs
new file mode 100644
index 0000000..6255f31
--- /dev/null
+++ b/src/CodeCasa.Notifications.Lights/LightNotificationManagerContext.cs
@@ -0,0 +1,35 @@
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using CodeCasa.Notifications.Lights.Config;
+
+namespace CodeCasa.Notifications.Lights
+{
+ ///
+ /// Represents a context that subscribes to light notifications from a manager and exposes them as an observable.
+ ///
+ public class LightNotificationManagerContext : IDisposable
+ {
+ private readonly BehaviorSubject _subject = new(null);
+ private readonly IDisposable _subscriptionDisposable;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The manager to subscribe to.
+ public LightNotificationManagerContext(LightNotificationManager lightNotificationManager)
+ {
+ _subscriptionDisposable = lightNotificationManager.LightNotifications().Subscribe(_subject);
+ }
+
+ ///
+ /// Gets an observable sequence of the current light notification configuration.
+ ///
+ public IObservable LightNotifications => _subject.AsObservable();
+
+ ///
+ public void Dispose()
+ {
+ _subscriptionDisposable.Dispose();
+ }
+ }
+}