<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jasper</title>
    <description>The latest articles on DEV Community by Jasper (@devjaspernl).</description>
    <link>https://dev.to/devjaspernl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2727536%2Fd55c759a-5625-43a2-90aa-ada6606d4ea9.png</url>
      <title>DEV Community: Jasper</title>
      <link>https://dev.to/devjaspernl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devjaspernl"/>
    <language>en</language>
    <item>
      <title>I Retired My Hue Bridge. Here's How I Rebuilt Everything It Did (and More) in C#</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Mon, 19 Oct 2026 13:04:18 +0000</pubDate>
      <link>https://dev.to/devjaspernl/i-retired-my-hue-bridge-heres-how-i-rebuilt-everything-it-did-and-more-in-c-4178</link>
      <guid>https://dev.to/devjaspernl/i-retired-my-hue-bridge-heres-how-i-rebuilt-everything-it-did-and-more-in-c-4178</guid>
      <description>&lt;h2&gt;
  
  
  📖 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;What You Lose Without the Bridge&lt;/li&gt;
&lt;li&gt;Rebuilding It in YAML&lt;/li&gt;
&lt;li&gt;Thinking in Layers&lt;/li&gt;
&lt;li&gt;
Building a Room, One Line at a Time

&lt;ul&gt;
&lt;li&gt;The Hallway&lt;/li&gt;
&lt;li&gt;The Office&lt;/li&gt;
&lt;li&gt;The Attic&lt;/li&gt;
&lt;li&gt;The Living Room&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;From Alarm to Notifications&lt;/li&gt;
&lt;li&gt;Seeing Which Layer Won&lt;/li&gt;
&lt;li&gt;Cheat Sheet&lt;/li&gt;
&lt;li&gt;Things to Know Before You Start&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Libraries&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 You can find the full source code of the libraries on &lt;a href="https://github.com/DevJasperNL/CodeCasa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Every room in this article is taken from my &lt;a href="https://github.com/DevJasperNL/CodeCasa.Showcase" rel="noopener noreferrer"&gt;showcase repository&lt;/a&gt;, a trimmed-down copy of my own home automation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;My smart home started the way many do: with a Philips Hue starter kit. I screwed in the bulbs, stuck a dimmer switch to the wall and everything just worked. The switch cycled through scenes, holding a button dimmed the lights, and the motion sensor in the hallway knew the difference between day and night.&lt;/p&gt;

&lt;p&gt;Then I discovered Home Assistant, and with it the freedom to connect my lights to everything else in the house. At some point I took the next step and retired the Hue Bridge altogether, pairing the bulbs, switches and sensors directly to my own Zigbee network through Zigbee2MQTT. Not because Hue fell short; for most homes it's good enough. I wanted my lights to behave exactly the way I had in mind, not like the closest option in an app.&lt;/p&gt;

&lt;p&gt;That's when I found out how much the bridge had been doing for me. All the behaviour I took for granted was gone, and rebuilding it turned out to be a lot more work than I expected, and a lot more fun. This article is about that work, about why it gets messy so quickly, and about the library I built to keep it manageable: &lt;strong&gt;CodeCasa.AutomationPipelines.Lights&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is the second article in a series about patterns for &lt;a href="https://netdaemon.xyz/" rel="noopener noreferrer"&gt;NetDaemon&lt;/a&gt;, which lets you write Home Assistant automations as C# applications, with entities exposed as strongly typed objects and state changes as observables. The &lt;a href="https://dev.to/devjaspernl/supercharging-home-assistant-automations-initial-states-and-boolean-logic-for-netdaemon-rx-3bd5"&gt;first one&lt;/a&gt; introduced stateful boolean observables. Every node in this article is driven by one, but you can follow along without having read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You Lose Without the Bridge
&lt;/h2&gt;

&lt;p&gt;Once your Hue devices talk to Home Assistant directly, a dimmer switch is no longer a dimmer switch. It's a device that emits button events. A motion sensor is a binary sensor. What you do with them is entirely up to you, which sounds great until you write down what the bridge used to do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pressing the &lt;strong&gt;on&lt;/strong&gt; button (the &lt;strong&gt;Hue&lt;/strong&gt; button on the newer switch) repeatedly cycles through scenes, as long as the presses follow each other quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holding&lt;/strong&gt; a button dims or brightens smoothly until you let go.&lt;/li&gt;
&lt;li&gt;The motion sensor uses different scenes for &lt;strong&gt;day and night&lt;/strong&gt;, and can be told to leave a room alone when the lights are already on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural light&lt;/strong&gt; shifts colour temperature and brightness through the day in a handful of time blocks.&lt;/li&gt;
&lt;li&gt;A whole room is addressed with &lt;strong&gt;one command&lt;/strong&gt;, not one per bulb.&lt;/li&gt;
&lt;li&gt;A bulb that was &lt;strong&gt;switched off at the wall&lt;/strong&gt; comes back in the state the room is in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And then there were the things the bridge never quite did, but that I had always wanted: turning a light off by hand should &lt;strong&gt;stick&lt;/strong&gt; instead of the motion sensor turning it right back on, and a doorbell or an alarm should be able to take the lights over for a moment and give them back afterwards.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; If you keep the Hue Bridge and use Home Assistant's Hue integration, none of this applies. The bridge keeps running its own logic. This article is about the situation where Home Assistant is the only brain left. You don't need Hue for any of it, either. Any bulb, button and motion sensor that shows up in Home Assistant has the same problem: several automations writing to one light, with nobody deciding who wins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Rebuilding It in YAML
&lt;/h2&gt;

&lt;p&gt;None of these features is hard to build on its own. Here is a motion automation with a day and night scene:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alias&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Hallway motion&lt;/span&gt;
  &lt;span class="na"&gt;triggers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
      &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;binary_sensor.hallway_motion&lt;/span&gt;
      &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;on"&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;choose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
              &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sun.sun&lt;/span&gt;
              &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;below_horizon&lt;/span&gt;
          &lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scene.turn_on&lt;/span&gt;
              &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scene.hallway_nightlight&lt;/span&gt;
      &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scene.turn_on&lt;/span&gt;
          &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scene.hallway_bright&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a second automation that turns the lights off two minutes after the last motion and you have a working room. But then you add the wall switch, and the first problem shows up. Someone switches the hallway to bright to find something in the cupboard, and two minutes later the motion automation turns the lights off on them. The fix is an &lt;code&gt;input_boolean.hallway_manual&lt;/code&gt; helper that the switch automation sets and both motion automations check.&lt;/p&gt;

&lt;p&gt;Now imagine you want the lights to turn red when the security alarm is triggered. Turning them red is one more automation. However, to make it work you now need to update every other automation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="c1"&gt;# inside each of the other automations:&lt;/span&gt;
  &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
      &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;input_boolean.hallway_manual&lt;/span&gt;
      &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;off"&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;not&lt;/span&gt;
      &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;state&lt;/span&gt;
          &lt;span class="na"&gt;entity_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;alarm_control_panel.home&lt;/span&gt;
          &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;triggered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This block has to go into the motion-on automation, the motion-off automation and every button of the switch automation, because any of them would happily overwrite the red light. And when the alarm ends, what should the room look like? Whatever it &lt;em&gt;would&lt;/em&gt; have looked like without the alarm, which no single automation knows.&lt;/p&gt;

&lt;p&gt;There are workarounds. You can snapshot the lights with &lt;code&gt;scene.create&lt;/code&gt; and restore them afterwards, but then you restore a moment from the past: if the sun set or everybody left during the alarm, the room comes back wrong. You can disable the other automations with &lt;code&gt;automation.turn_off&lt;/code&gt;, but then the triggers that fired in the meantime are lost. Both work until they don't. Blueprints such as the built-in &lt;em&gt;Motion-activated Light&lt;/em&gt; and integrations like &lt;em&gt;Adaptive Lighting&lt;/em&gt; each solve one of these problems well. What none of them give you is a place where the pieces meet.&lt;/p&gt;

&lt;p&gt;The underlying issue is that every automation writes to the light directly, and nothing decides who wins. Home Assistant gives you triggers, conditions and actions, but it has no built-in notion of priority between automations. So every cross-cutting concern ends up as a condition in all of them. What is missing is not another trigger. It is a model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thinking in Layers
&lt;/h2&gt;

&lt;p&gt;Getting a motion sensor, a switch and an alarm to share one bulb without tripping over each other is a proper design problem, and the kind I enjoy most. The model I ended up with is a pipeline per light. A pipeline is an ordered list of nodes, and each node represents one concern, a layer of behaviour: motion, the switch, the alarm. A node receives the output of the node before it and then does one of two things. It either sets its own output, or it &lt;strong&gt;passes through&lt;/strong&gt; what it received, which is its way of saying "I have no opinion right now". What sets this apart from an ordinary pipeline is that a node does not only react to its input. Like a NetDaemon app, every node is an automation of its own: it can watch a sensor, a button or a timer and change its output at any moment. That is why the library calls these automation pipelines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasdnfets8rpxrox9ilfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fasdnfets8rpxrox9ilfe.png" alt="Pipeline from default off through the motion, dimmer switch and alarm layers to light.hallway_light. At night with motion, motion outputs night light and the later layers pass it through." width="800" height="290"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Later nodes win over earlier ones, so the order of the list &lt;em&gt;is&lt;/em&gt; the priority. Every pipeline starts with a default value, off, which is what the first node receives as its input. A pipeline in which every node passes through therefore turns the light off. The light is only ever driven from the end of the pipeline, and no node knows about any other node. The motion layer keeps tracking motion while the alarm is active, so when the alarm node goes back to passing through, the room is immediately in the state it should be in at that moment. Nothing needs to be restored.&lt;/p&gt;

&lt;p&gt;There is a third thing a node can do: take what it receives and change it. &lt;code&gt;LimitBrightnessWhen(night, 100)&lt;/code&gt; caps the brightness of every layer before it, whatever scene they picked, and leaves colour and off alone. Most of this article is about the first two, but it is good to know the option exists.&lt;/p&gt;

&lt;p&gt;The pipeline machinery lives in a small generic library, &lt;code&gt;CodeCasa.AutomationPipelines&lt;/code&gt;. On top of it, &lt;code&gt;CodeCasa.AutomationPipelines.Lights&lt;/code&gt; adds a fluent API for lights, and &lt;code&gt;CodeCasa.AutomationPipelines.Lights.NetDaemon&lt;/code&gt; adds overloads for Home Assistant entities and scenes. That last package is the one to install. Registering everything used in this article looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNetDaemonScheduler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// NetDaemon's IScheduler; everything time-based uses it&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLightPipelines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLightScenes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// only if you pass Home Assistant scene entities (The Living Room)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddLightNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// only for the notification layer (From Alarm to Notifications)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything time-based in the library takes an &lt;code&gt;IScheduler&lt;/code&gt; from System.Reactive. In production that's the normal clock; in tests it's a virtual one, so a five-minute timeout can be tested in a millisecond.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Room, One Line at a Time
&lt;/h2&gt;

&lt;p&gt;Four rooms from the &lt;a href="https://github.com/DevJasperNL/CodeCasa.Showcase" rel="noopener noreferrer"&gt;showcase&lt;/a&gt; are enough to rebuild the list from the start of this article: the hallway, the office, the attic and the living room.&lt;/p&gt;

&lt;p&gt;A word on the inputs first. NetDaemon generates classes such as &lt;code&gt;LightEntities&lt;/code&gt;, &lt;code&gt;SensorEntities&lt;/code&gt; and &lt;code&gt;SceneEntities&lt;/code&gt; from your Home Assistant instance, so &lt;code&gt;lightEntities.HallwayLight&lt;/code&gt; &lt;em&gt;is&lt;/em&gt; &lt;code&gt;light.hallway_light&lt;/code&gt;. An app is a class marked &lt;code&gt;[NetDaemonApp]&lt;/code&gt;, and NetDaemon constructs it through dependency injection, which is where every constructor parameter below comes from. Without the bridge, a switch is just a Zigbee2MQTT device publishing button events. In the showcase these are wrapped in small classes that I register alongside the apps: &lt;code&gt;HallwayWallSwitch&lt;/code&gt; is an &lt;code&gt;IObservable&amp;lt;Unit&amp;gt;&lt;/code&gt; that emits on every press (&lt;code&gt;Unit&lt;/code&gt; is Rx's way of saying "something happened, no value attached"), and &lt;code&gt;OfficeDimmerSwitch&lt;/code&gt; exposes &lt;code&gt;OnOffPressed&lt;/code&gt;, &lt;code&gt;ScenePressed&lt;/code&gt;, &lt;code&gt;Dimming&lt;/code&gt; and &lt;code&gt;Brightening&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Wherever the API wants a light state, it accepts one of three things: a &lt;code&gt;LightParameters&lt;/code&gt; object (brightness 0 to 255, a colour or a colour temperature; &lt;code&gt;Bright&lt;/code&gt;, &lt;code&gt;Relax&lt;/code&gt;, &lt;code&gt;NightLight&lt;/code&gt;, &lt;code&gt;Concentrate&lt;/code&gt; and &lt;code&gt;Dimmed&lt;/code&gt; are built-in presets you can overwrite), a Home Assistant scene entity, or a timeline. Internally everything becomes a &lt;code&gt;LightTransition&lt;/code&gt;, parameters plus a transition time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Hallway
&lt;/h3&gt;

&lt;p&gt;If you read part one, this is the upstairs hallway light from that article, now as a pipeline.&lt;/p&gt;

&lt;h4&gt;
  
  
  Motion
&lt;/h4&gt;

&lt;p&gt;The hallway has a Hue motion sensor, which Zigbee2MQTT exposes as an occupancy binary sensor and an illuminance sensor. &lt;code&gt;MotionSensor&lt;/code&gt; from &lt;code&gt;CodeCasa.NetDaemon.Sensors.Composite&lt;/code&gt; combines the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HallwayMotionSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;BinarySensorEntities&lt;/span&gt; &lt;span class="n"&gt;binarySensorEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SensorEntities&lt;/span&gt; &lt;span class="n"&gt;sensorEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;MotionSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;binarySensorEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HallwayMotionSensorOccupancy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sensorEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HallwayMotionSensorIlluminance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result is a stateful &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt;. Stateful means it emits its current value the moment you subscribe, not only on the next change, so a pipeline that starts at eleven at night immediately knows that nobody is in the hallway. It becomes &lt;code&gt;true&lt;/code&gt; when there is movement and the room is dark, and stays &lt;code&gt;true&lt;/code&gt; until a minute after the last movement. That one observable replaces the motion-on automation, the motion-off automation and the illuminance condition.&lt;/p&gt;

&lt;h4&gt;
  
  
  Day and Night
&lt;/h4&gt;

&lt;p&gt;The pipeline is a single &lt;code&gt;SwitchWhen&lt;/code&gt;. It takes the observable that activates the node and a second one that chooses between two outcomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;night&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunsets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunrises&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HallwayLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hallwayMotionSensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;night&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NightLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;night&lt;/code&gt; comes from &lt;a href="https://dev.to/devjaspernl/occurify-fluent-time-modeling-for-net-developers-3e4n"&gt;Occurify&lt;/a&gt;, my library for modelling time as instants and periods. &lt;code&gt;AstroInstants.LocalSunsets&lt;/code&gt; is the recurring sunset for the coordinates you configure once at startup, &lt;code&gt;PeriodTimeline.Between&lt;/code&gt; turns sunsets and sunrises into night periods, and &lt;code&gt;ToBooleanObservable&lt;/code&gt; makes that a stateful observable that is &lt;code&gt;true&lt;/code&gt; during those periods. If you'd rather not, &lt;code&gt;sun.sun&lt;/code&gt; as a boolean observable works too.&lt;/p&gt;

&lt;p&gt;While the motion sensor is &lt;code&gt;false&lt;/code&gt; the node passes through, and since there is nothing before it the pipeline falls back to its default, which is off. Because both observables are stateful, this works at startup and at the moment the sun sets while somebody is in the hallway. The &lt;code&gt;choose&lt;/code&gt; block from the YAML version is gone. Without the day and night distinction it would simply be &lt;code&gt;When(hallwayMotionSensor, LightParameters.Bright)&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Bright for Five Minutes
&lt;/h4&gt;

&lt;p&gt;The hallway also has a wall switch. Sometimes you want proper light for a while, to find something in the cupboard, and then you want the hallway to go back to doing its own thing. The switch gets its own node, placed after the motion node, so it wins:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hallwayWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAutoPassThroughLightNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lambda is a &lt;em&gt;node factory&lt;/em&gt;: a function that receives the service provider and builds the node when the toggle activates, so every press gets a fresh five-minute timer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddToggle&lt;/code&gt; decides from the actual state of the light. If it's off, a press activates the node: bright, for five minutes. After that the node does not turn the light off. It starts passing through. If there is still motion, the hallway falls back to the motion scene. If not, it goes dark. If the light is already on when the switch is pressed, from motion or from an earlier press, it turns off and stays off until the motion layer changes. There's also &lt;code&gt;CreateAutoOffLightNode&lt;/code&gt; for when the timeout should turn the light off instead.&lt;/p&gt;

&lt;p&gt;This is also where the &lt;code&gt;input_boolean.hallway_manual&lt;/code&gt; helper went. While the switch node is active, the motion layer keeps doing its thing underneath, but its output never reaches the light. When motion ends it passes through, which changes nothing, because the switch layer is still in charge. Nothing needs to check a flag.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; Deciding from the actual state has one catch. Walk into the hallway at night, the motion sensor turns the light on, and half a second later you press the switch, because you wanted proper light anyway. A plain toggle would see "on" and turn the light off. &lt;code&gt;AddToggle&lt;/code&gt; therefore has a grace window, one second by default: if the light changed within that window and this toggle was not the cause, the press is judged against the state &lt;em&gt;before&lt;/em&gt; the change. &lt;code&gt;SetGracePeriod&lt;/code&gt; on the toggle configurator changes the window.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;AddInteractionNode&lt;/code&gt; does one more thing: it watches the light itself. When somebody turns it off from outside the pipeline, with the Home Assistant app for example, the node holds it off until something earlier in the pipeline changes. Turning a light off by hand sticks. With &lt;code&gt;HoldExternalChanges&lt;/code&gt; enabled it does the same for a scene or brightness set from outside, optionally for a limited time, so your existing YAML automations and the pipeline can coexist.&lt;/p&gt;

&lt;p&gt;Put together, the whole app is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NetDaemonApp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HallwayLightsApp&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;HallwayLightsApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;LightPipelineFactory&lt;/span&gt; &lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LightEntities&lt;/span&gt; &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HallwayMotionSensor&lt;/span&gt; &lt;span class="n"&gt;hallwayMotionSensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;HallwayWallSwitch&lt;/span&gt; &lt;span class="n"&gt;hallwayWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;night&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunsets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunrises&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HallwayLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hallway"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SwitchWhen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hallwayMotionSensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;night&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NightLight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
                &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hallwayWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAutoPassThroughLightNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)))));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnvqfjwcrp8fz449mhle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdnvqfjwcrp8fz449mhle.png" alt="Hallway pipeline: default off, motion layer passing through, interaction node turning the light bright after a wall switch press." width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the whole hallway, in &lt;a href="https://github.com/DevJasperNL/CodeCasa.Showcase/blob/main/src/CodeCasa.Automations/Apps/Lights/Hallway/HallwayLightsApp.cs" rel="noopener noreferrer"&gt;one file&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Office
&lt;/h3&gt;

&lt;h4&gt;
  
  
  The Wall Switch and the Dimmer
&lt;/h4&gt;

&lt;p&gt;The office has a wall switch and a Hue dimmer switch. Its pipeline doesn't use motion; I sit still for long enough that motion would be wrong more often than right. The complete pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeLights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseLightGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeLightsZ2m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;officeWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;officeDimmerSwitch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnOffPressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddCycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;officeDimmerSwitch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScenePressed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Concentrate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Relax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddReactiveDimmer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;officeDimmerSwitch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOffWhenLastPersonToAsleepOrAway&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a lot of Hue behaviour in these few lines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The two &lt;code&gt;AddToggle&lt;/code&gt; calls make the wall switch and the &lt;strong&gt;on&lt;/strong&gt; button of the dimmer toggle between off and bright.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddCycle&lt;/code&gt; is the scene button. Where the bridge counts presses, this looks at the &lt;strong&gt;actual state&lt;/strong&gt; of the lights on every press to decide which scene comes next, with some tolerance, because a bulb asked for 255 will happily report 254. If the state isn't recognised, the cycle starts at the first scene. &lt;code&gt;LightParameters.Bright&lt;/code&gt; and friends are built-in scenes, plain parameter sets really; the living room below uses Home Assistant scenes instead.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AddReactiveDimmer&lt;/code&gt; implements hold to dim, and does a little more than the bridge did. More on that below.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TurnOffWhenLastPersonToAsleepOrAway&lt;/code&gt; is not from the library. It's a &lt;a href="https://github.com/DevJasperNL/CodeCasa.Showcase/blob/main/src/CodeCasa.CustomEntities.Automation/Extensions/LightTransitionReactiveNodeConfiguratorExtensions.cs" rel="noopener noreferrer"&gt;ten-line extension method&lt;/a&gt; in the showcase, built on &lt;code&gt;AddNodeSource&lt;/code&gt;, the primitive that all of the above is sugar for. Your own concerns slot in next to the built-in ones.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Holding the Button
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;AddReactiveDimmer&lt;/code&gt; takes an &lt;code&gt;IDimmer&lt;/code&gt;, a tiny interface with two observables, &lt;code&gt;Dimming&lt;/code&gt; and &lt;code&gt;Brightening&lt;/code&gt;, which &lt;code&gt;OfficeDimmerSwitch&lt;/code&gt; implements. While one of them is &lt;code&gt;true&lt;/code&gt;, the brightness changes by a fifth of the 0 to 255 range: once straight away, then every 400 ms. Only the brightness changes; colours stay as they are. The dimmer starts from whatever the interaction node is showing, so you dim the current scene, and it resets as soon as the interaction node switches to something else, so the next scene press shows that scene at its own brightness again.&lt;/p&gt;

&lt;p&gt;With a single bulb that is all there is to it. With several bulbs it gets more interesting. Holding the dim button brings all bulbs down together until each reaches the minimum brightness, 2 out of 255 by default, where it waits for the others. Once all of them are at the minimum, the next step turns one bulb off, the step after that the next one, until the last bulb goes out. Holding the brighten button does the same in reverse: from a dark room, the bulbs come on at the minimum one at a time, in the opposite order, and once all of them are on they rise together to full brightness. You can dim a room down to one soft bulb in the corner without reaching for an app.&lt;/p&gt;

&lt;p&gt;None of this is something you could have asked the bridge for. Nobody needs their lights to go out one by one, but this is the fun part of owning every step between the button and the bulb: when you have an idea of how a room should behave, you write it down.&lt;/p&gt;

&lt;p&gt;By default the bulbs go out in the order the group lists them in Home Assistant. To choose your own order, and to change the minimum, the step size or the speed, pass &lt;code&gt;DimmerOptions&lt;/code&gt; along with the dimmer. &lt;code&gt;AsLight()&lt;/code&gt; wraps a Home Assistant entity as the library's &lt;code&gt;ILight&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dimmerOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DimmerOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;MinBrightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;dimmerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetLightOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeCeilingLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLight&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeWindowLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLight&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeDeskLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsLight&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;  &lt;span class="c1"&gt;// the desk lamp stays on longest&lt;/span&gt;

&lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeLights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseLightGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeLightsZ2m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="c1"&gt;// toggles and cycle as before&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddReactiveDimmer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;officeDimmerSwitch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetReactiveDimmerOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimmerOptions&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The order must list exactly the bulbs in the pipeline, the bulbs themselves rather than the group, or setup throws.&lt;/p&gt;

&lt;h4&gt;
  
  
  One Command for the Whole Room
&lt;/h4&gt;

&lt;p&gt;That leaves one item on the list for the office. &lt;code&gt;lightEntities.OfficeLights&lt;/code&gt; is a Home Assistant light group, a software list of three bulbs. The library flattens it and builds one pipeline per bulb, because scenes can give every bulb its own colour and, as you'll see later, notifications can choose which bulbs they apply to. The downside is that a scene change results in three separate Zigbee commands instead of one. That's more traffic on the network, and if one of those commands is delayed or dropped, the bulbs end up in different states. The bridge avoided this by addressing the Zigbee group. &lt;code&gt;UseLightGroup&lt;/code&gt;, the first line of the pipeline, does the same with &lt;code&gt;OfficeLightsZ2m&lt;/code&gt;, a group created in Zigbee2MQTT that Zigbee itself understands, so one command reaches all bulbs. The pipeline's lights must be exactly the members of that group. The library can only check this when Home Assistant lists the group's members, which it doesn't for a Zigbee2MQTT group, so keeping the two in sync is up to you.&lt;/p&gt;

&lt;p&gt;Each bulb's output is held back for a very short window, 20 ms by default. If all members of the group ask for the same thing within that window, a single command goes to the group. If they differ, for instance because a scene gives every bulb its own colour, each bulb is driven individually. You get the single command when it's possible and the per-bulb control when it's needed. The dimmer from the previous section plays along: while all bulbs dim together from the same scene, each step is one group command; once they start going out one by one, each bulb is driven on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoon5cyapuqtoug68q7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdoon5cyapuqtoug68q7w.png" alt="Three bulb pipelines each ending in a group node; a 20 ms window sends one command to the Zigbee2MQTT group when all outputs are identical, one per bulb otherwise." width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One more bridge feature comes for free here. A bulb that is switched off at the wall misses every command sent while it was gone. When it reports available again, the pipeline re-applies its current output, so the bulb joins the room in the state it should be in. This is on by default and can be turned off with &lt;code&gt;ReapplyOutputWhenAvailable(false)&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; If every bulb in a room always shows the same thing, you can skip the split and the 20 ms wait altogether by setting up the pipeline for the Zigbee2MQTT group itself: &lt;code&gt;SetupLightPipeline(lightEntities.OfficeLightsZ2m, ...)&lt;/code&gt;. The library only splits a light into bulbs when Home Assistant lists its members in an &lt;code&gt;entity_id&lt;/code&gt; attribute, which a light group has and a Zigbee2MQTT group doesn't, so this gives you a single pipeline that drives the group entity. You give up everything that works per bulb: scenes with a colour per bulb, notifications on some of the bulbs, the dimmer's one-at-a-time behaviour, noticing that one bulb was turned off by hand, and re-applying the output to one bulb that comes back from the wall switch. Pick one of the two setups per room; two pipelines driving the same bulbs bring back the question of who wins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The Attic
&lt;/h3&gt;

&lt;p&gt;Hue's natural light can be rebuilt with an Occurify timeline. A timeline maps moments of the day to light parameters, and the pipeline interpolates between them continuously. In the attic the wall switch toggles a timeline instead of a fixed scene:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ITimelineConfigurator&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;naturalLight&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tl&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tl&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunrises&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunsets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OffsetHours&lt;/span&gt;&lt;span class="p"&gt;(-&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunsets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeZoneInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dimmed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AtticLights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseLightGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AtticLightsZ2m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;atticWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;naturalLight&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From sunrise until two hours before sunset the attic is bright. Over the last two hours of daylight it drifts to dimmed, slowly enough that nobody notices, stays dimmed until five in the morning, and ramps back up to bright at sunrise. Between two entries the pipeline always interpolates, so holding a value means adding it twice: once where the hold starts and once where it ends. Where Hue's natural light steps through time blocks, this is one continuous ramp, and when a ramp involves colour the blend is gamma-corrected, with a gamma I tuned against my own Hue bulbs until it matched their native transition. Timelines are accepted wherever parameters are: &lt;code&gt;When(motion, naturalLight)&lt;/code&gt; for a motion layer, or as an entry in a toggle or cycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Living Room
&lt;/h3&gt;

&lt;p&gt;The living room scenes were made in the Home Assistant scene editor, with a different colour per bulb, and I wanted to keep them. The NetDaemon package accepts scene entities everywhere light parameters are accepted, provided &lt;code&gt;AddLightScenes()&lt;/code&gt; is registered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightPipelineFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetupLightPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomLights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;UseLightGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomLightsZ2m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddInteractionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddToggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;livingRoomWallSwitch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sceneEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomAmbiance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sceneEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomRelax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;sceneEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBright&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOffWhenLastPersonToAsleepOrAway&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNotifications&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A toggle with several entries is the Hue &lt;strong&gt;on&lt;/strong&gt; button: the first press gives the first scene, presses within a second of each other walk through the others and wrap around, and a press after a pause turns the lights off. Unlike &lt;code&gt;AddCycle&lt;/code&gt;, the toggle's main job is on and off; the extra scenes are only reachable by pressing again quickly. &lt;code&gt;SetToggleTimeout&lt;/code&gt; changes the window. Each scene is read from Home Assistant once, and each bulb's pipeline picks its own part of it.&lt;/p&gt;

&lt;p&gt;The last line, &lt;code&gt;AddNotifications&lt;/code&gt;, is where the alarm from the YAML chapter ends up.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Alarm to Notifications
&lt;/h2&gt;

&lt;p&gt;In a pipeline, the alarm from the YAML chapter could simply be one last node, &lt;code&gt;When(alarmTriggered, red)&lt;/code&gt;, that wins over everything and knows nothing about motion, switches or timeouts.&lt;/p&gt;

&lt;p&gt;But an alarm is just one reason to take over the lights for a while. The doorbell, the washing machine and the smoke detector are others, and none of them belong in the living room code. For this there is a separate package, &lt;code&gt;CodeCasa.Notifications.Lights&lt;/code&gt;, registered with &lt;code&gt;AddLightNotifications()&lt;/code&gt; as shown earlier. Every pipeline that should show notifications opts in with the line you saw in the living room:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNotifications&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any other application can now raise a notification, without knowing which lights or pipelines exist. It shows up as the last layer of every pipeline that opted in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r6x2h9cwri84ud6rgyv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3r6x2h9cwri84ud6rgyv.png" alt="Living room pipeline: interaction node outputs relax, the notifications node shows the doorbell blinking blue and white over it." width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the showcase, the doorbell is the one raising notifications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;NetDaemonApp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;internal&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DoorbellLightNotifications&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;DoorbellLightNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;FrontDoorDoorbell&lt;/span&gt; &lt;span class="n"&gt;frontDoorDoorbell&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;LightNotificationContext&lt;/span&gt; &lt;span class="n"&gt;lightNotifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;notificationId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DoorbellLightNotifications&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;frontDoorDoorbell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BellPressed&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PersistTrueFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrueFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightNotifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notificationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BlinkNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;White&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightNotifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RemoveNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notificationId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BellPressed&lt;/code&gt; is a binary sensor that is only on for a moment; &lt;code&gt;PersistTrueFor&lt;/code&gt; stretches it to thirty seconds. A notification is a node factory, so it isn't limited to a fixed colour. The alarm would be a one-liner with a higher priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightNotifications&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Alarm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Brightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RgbColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Red&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;AsPipelineNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BlinkNode&lt;/code&gt; is a custom node, and writing one is not much work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlinkNode&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LightTransitionNode&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IDisposable&lt;/span&gt; &lt;span class="n"&gt;_timer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BlinkNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;firstColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="n"&gt;secondColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Blink"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;LightTransition&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;states&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Brightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RgbColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;firstColor&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;AsTransition&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LightParameters&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Brightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RgbColor&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secondColor&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;AsTransition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;_timer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Observable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)((&lt;/span&gt;&lt;span class="n"&gt;tick&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="n"&gt;ValueTask&lt;/span&gt; &lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_timer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Dispose&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisposeAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A node sets &lt;code&gt;Output&lt;/code&gt;, or calls &lt;code&gt;PassInputThrough()&lt;/code&gt; when it has no opinion. A node that cares about what comes from upstream overrides &lt;code&gt;InputReceived&lt;/code&gt;; &lt;code&gt;BlinkNode&lt;/code&gt; doesn't, so it ignores its input until it's disposed. The base class takes care of thread safety, so setting &lt;code&gt;Output&lt;/code&gt; from a timer is fine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; A node that only needs services doesn't need a lambda. &lt;code&gt;AddNode&amp;lt;TNode&amp;gt;()&lt;/code&gt; lets the library build it the same way NetDaemon builds your apps, filling in its constructor from the services you registered. It does so once, when the pipeline is set up; &lt;code&gt;When&amp;lt;TNode&amp;gt;(observable)&lt;/code&gt; and &lt;code&gt;Add&amp;lt;TNode&amp;gt;()&lt;/code&gt; in a toggle build a fresh node every time the layer activates. Because a group runs one pipeline per bulb, each copy can also ask for the bulb it controls, &lt;code&gt;ILight&lt;/code&gt;, and for &lt;code&gt;LightPipelineContext&lt;/code&gt;, which holds what the pipeline last sent to that bulb. In a lambda, the same services are a &lt;code&gt;sp.GetRequiredService&amp;lt;ILight&amp;gt;()&lt;/code&gt; away.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When several notifications are active, the one with the highest priority is shown. When the last one is removed, every pipeline disposes its copy of the node, the notification layer passes through, and the room shows whatever the layers below have worked out in the meantime. If everybody left while the doorbell was blinking, the lights go off. If somebody picked a scene, that scene appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Seeing Which Layer Won
&lt;/h2&gt;

&lt;p&gt;With several layers, "why is this light on?" becomes a fair question. Every pipeline can log each hop between nodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hallway"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hallway app does this. Each line carries the light, the pipeline path, the node that produced the value and the value itself, so you can read which layer produced the final result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[light.hallway_light] Hallway: Passing [Node 1] (Interaction node) value [temperature 4000 (255)] to pipeline output.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lines are logged at Trace level, which is off by default, so enable it for the &lt;code&gt;CodeCasa&lt;/code&gt; namespace when you need it. The same telemetry is available as data through &lt;code&gt;AddTelemetrySubscriber&lt;/code&gt;, and the &lt;code&gt;CodeCasa.AutomationPipelines.Lights.Mqtt&lt;/code&gt; package publishes a JSON snapshot of a pipeline's state over MQTT on every change, if you opt the pipeline in with &lt;code&gt;PublishTelemetryToMqtt&lt;/code&gt;. From there, an MQTT sensor in Home Assistant can show on a dashboard which layer is driving each light.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;The list from the start of this article, plus a few things that came up along the way. Some of these did not make it into the text:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you want&lt;/th&gt;
&lt;th&gt;Pipeline equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Button cycles through scenes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddCycle(trigger, scenes...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Button toggles on and off&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddToggle(trigger, scene)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hold to dim or brighten&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddReactiveDimmer(dimmer)&lt;/code&gt; in the interaction node&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Motion with day and night scenes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SwitchWhen(motion, isNight, night, day)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Natural light&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddToggle(trigger, timeline)&lt;/code&gt;, &lt;code&gt;When(observable, timeline)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Whole room changes at once&lt;/td&gt;
&lt;td&gt;&lt;code&gt;UseLightGroup(group)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bulb was unreachable for a moment&lt;/td&gt;
&lt;td&gt;re-applied automatically when it comes back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual off sticks&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddInteractionNode()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual scene sticks too&lt;/td&gt;
&lt;td&gt;&lt;code&gt;AddInteractionNode(new InteractionNodeOptions { HoldExternalChanges = true })&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporary override&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CreateAutoPassThroughLightNode&lt;/code&gt;, &lt;code&gt;CreateAutoOffLightNode&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Doorbell, alarm, anything that takes the lights over&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddNotifications()&lt;/code&gt; + &lt;code&gt;LightNotificationContext.Notify(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own concern (everyone asleep, nobody home)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddNodeSource(...)&lt;/code&gt; in an extension method&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own node, built by dependency injection&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AddNode&amp;lt;TNode&amp;gt;()&lt;/code&gt;, &lt;code&gt;When&amp;lt;TNode&amp;gt;(observable)&lt;/code&gt;, &lt;code&gt;Add&amp;lt;TNode&amp;gt;()&lt;/code&gt; in a toggle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cap brightness at night&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LimitBrightnessWhen(isNight, 100)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Some lights in a room behave differently&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ForLights(ids, configure)&lt;/code&gt; inside the pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Things to Know Before You Start
&lt;/h2&gt;

&lt;p&gt;This approach isn't for everyone, and I'd rather say so here than in the comments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If YAML or the automation editor covers your needs, stay there. This needs C# and NetDaemon running next to Home Assistant.&lt;/li&gt;
&lt;li&gt;If you still have a Hue Bridge and are happy with it, keep it. It does most of this out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is for when you want your lights to do something no app offers, or when, like me, you enjoy building it.&lt;/p&gt;

&lt;p&gt;And a few things to know if you do go ahead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restarts.&lt;/strong&gt; When NetDaemon starts, every pipeline begins at its default, off, and applies it. Layers driven by stateful observables re-evaluate immediately, so a hallway with motion or a room under a timeline comes straight back. Anything that only exists because of a button press, a scene you cycled to or a five-minute override, is gone, and that room goes dark.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scenes&lt;/strong&gt; made in the scene editor are read from Home Assistant once and cached. After editing a scene, you restart your apps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;UseLightGroup&lt;/code&gt;&lt;/strong&gt; requires the pipeline's lights to be exactly the members of the group. For a Zigbee2MQTT group the library can't check this, so keep the two in sync yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your existing automations&lt;/strong&gt; keep working. To a pipeline they look like somebody changing the light by hand, so a manual off sticks and anything else is overwritten on the next upstream change, unless you enable &lt;code&gt;HoldExternalChanges&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Everything runs on an &lt;code&gt;IScheduler&lt;/code&gt;&lt;/strong&gt;, which NetDaemon provides with &lt;code&gt;AddNetDaemonScheduler()&lt;/code&gt;. In tests you swap in a &lt;code&gt;TestScheduler&lt;/code&gt; and run a whole evening of motion, presses and timeouts in virtual time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Moving away from the Hue Bridge showed me how much behaviour sits in a product that "just works". Rebuilding that behaviour wasn't the hard part. Keeping the pieces from getting in each other's way was.&lt;/p&gt;

&lt;p&gt;Separating concerns into layers changed how I think about automating my home. Adding the alarm was one line, and removing it again would be one line too. Every new idea since has been a question of where in the pipeline it belongs, not which existing automations need another condition.&lt;/p&gt;

&lt;p&gt;And it has been fun: deciding how a dimmer should behave across three bulbs, working out who should win when the doorbell rings, and then seeing the answer in my own living room. If that sounds appealing, I hope these libraries give you a head start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Libraries
&lt;/h2&gt;

&lt;p&gt;All libraries in this article are open source under the MIT license and available on NuGet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.AutomationPipelines.Lights" rel="noopener noreferrer"&gt;CodeCasa.AutomationPipelines.Lights&lt;/a&gt;: the light pipelines&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.AutomationPipelines.Lights.NetDaemon" rel="noopener noreferrer"&gt;CodeCasa.AutomationPipelines.Lights.NetDaemon&lt;/a&gt;: overloads for NetDaemon entities and scenes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.Lights.NetDaemon.Scenes" rel="noopener noreferrer"&gt;CodeCasa.Lights.NetDaemon.Scenes&lt;/a&gt;: reads Home Assistant scenes into light parameters&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.Lights.Timelines" rel="noopener noreferrer"&gt;CodeCasa.Lights.Timelines&lt;/a&gt;: Occurify timelines as light transitions&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.AutomationPipelines.Lights.Mqtt" rel="noopener noreferrer"&gt;CodeCasa.AutomationPipelines.Lights.Mqtt&lt;/a&gt;: pipeline telemetry over MQTT&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.Notifications.Lights" rel="noopener noreferrer"&gt;CodeCasa.Notifications.Lights&lt;/a&gt;: notifications as a pipeline layer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.NetDaemon.Extensions.Observables" rel="noopener noreferrer"&gt;CodeCasa.NetDaemon.Extensions.Observables&lt;/a&gt;: the boolean observables from part one (renamed since)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nuget.org/packages/CodeCasa.NetDaemon.Sensors.Composite" rel="noopener noreferrer"&gt;CodeCasa.NetDaemon.Sensors.Composite&lt;/a&gt;: the motion sensor that combines occupancy and illuminance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can explore the full source code on &lt;a href="https://github.com/DevJasperNL/CodeCasa" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. The rooms in this article live in the &lt;a href="https://github.com/DevJasperNL/CodeCasa.Showcase" rel="noopener noreferrer"&gt;CodeCasa.Showcase&lt;/a&gt; repository, together with the dashboard and the other automations of my house.&lt;/p&gt;

&lt;p&gt;If you find CodeCasa helpful or interesting, a ⭐️ on the repo would mean a lot!&lt;/p&gt;

</description>
      <category>homeassistant</category>
      <category>csharp</category>
      <category>dotnet</category>
      <category>smarthome</category>
    </item>
    <item>
      <title>Occurify: Fluent Time Modeling for .NET Developers</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Sat, 05 Apr 2025 12:11:19 +0000</pubDate>
      <link>https://dev.to/devjaspernl/occurify-fluent-time-modeling-for-net-developers-3e4n</link>
      <guid>https://dev.to/devjaspernl/occurify-fluent-time-modeling-for-net-developers-3e4n</guid>
      <description>&lt;h2&gt;
  
  
  📖 Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;Usage&lt;/li&gt;
&lt;li&gt;
Design

&lt;ul&gt;
&lt;li&gt;Instant&lt;/li&gt;
&lt;li&gt;Period&lt;/li&gt;
&lt;li&gt;Instant Timeline&lt;/li&gt;
&lt;li&gt;Period Timeline&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Source Code&lt;/li&gt;

&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 You can find the full source code for Occurify on &lt;a href="https://github.com/Occurify/Occurify" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I’ve always found working with time in code way more complicated than it needs to be.&lt;/p&gt;

&lt;p&gt;Whether it’s dealing with schedules, recurring events, or aligning actions across time zones, time-based logic quickly becomes messy, repetitive, and full of edge cases. I wanted a way to think about time more conceptually—to express ideas like “every sunset” or “weekdays between 9 and 5” without getting bogged down in hardcoded timestamps and brittle date math.&lt;/p&gt;

&lt;p&gt;That’s why I built Occurify — a powerful and intuitive .NET library for defining, filtering, transforming, and scheduling time in a more human-friendly and composable way.&lt;/p&gt;

&lt;p&gt;Occurify lets you break down time into clear building blocks: instants, periods, and timelines. It helps you reason about time the way you actually think about it—and then turns those ideas into clean, reliable code.&lt;/p&gt;

&lt;p&gt;Whether you're working with time zones, cron expressions, solar events like sunrise and sunset, or even reactive scheduling, Occurify is designed to help you build smarter, more flexible time-driven systems.&lt;/p&gt;

&lt;p&gt;In this article, I’ll walk you through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How Occurify models time using instants, periods, and timelines&lt;/li&gt;
&lt;li&gt;Defining dynamic and reusable time-based concepts (like "20 minutes after sunset")&lt;/li&gt;
&lt;li&gt;Filtering, transforming, and composing timelines with a fluent API&lt;/li&gt;
&lt;li&gt;Randomizing timing and filtering by patterns like weekdays or holidays&lt;/li&gt;
&lt;li&gt;Scheduling real-world actions using ReactiveX&lt;/li&gt;
&lt;li&gt;How the design of Occurify makes all these feats possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Instead of dealing with fixed timestamps, Occurify lets you think about time in a more &lt;strong&gt;human-friendly&lt;/strong&gt; way. You don't need to precompute every possible event—just define the concept of an event, like &lt;em&gt;"all sunsets,"&lt;/em&gt; and let Occurify handle the rest.&lt;/p&gt;

&lt;p&gt;As an example, let’s imagine we want to automate our lights to turn on in the evening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defining Timelines
&lt;/h3&gt;

&lt;p&gt;Instead of manually maintaining a list of sunset times, we can simply use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;sunsets&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AstroInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LocalSunsets&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This timeline now represents every sunset dynamically—no need for hardcoded schedules.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transforming Timelines
&lt;/h3&gt;

&lt;p&gt;Want to schedule events &lt;strong&gt;20 minutes after sunset&lt;/strong&gt;? Just shift the timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;twentyMinAfterSunset&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunsets&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;code&gt;twentyMinAfterSunset&lt;/code&gt; dynamically represents every sunset, plus 20 minutes—no manual calculations needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Combining Timelines Into Periods
&lt;/h3&gt;

&lt;p&gt;Now, let’s define a time when the lights should &lt;strong&gt;turn off&lt;/strong&gt; and create a period from &lt;strong&gt;20 minutes after sunset&lt;/strong&gt; until &lt;strong&gt;11 PM&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;elevenPm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeZoneInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;23&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;lightOnPeriods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;twentyMinAfterSunset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elevenPm&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this, lightOnPeriods now represents &lt;strong&gt;all the evening periods&lt;/strong&gt; when the lights should be on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Filtering &amp;amp; Randomization
&lt;/h3&gt;

&lt;p&gt;If you want the lights to turn on &lt;strong&gt;only on weekdays&lt;/strong&gt;, you can filter the periods like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightOnPeriods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Within&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeZonePeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Workdays&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make the timing feel more natural, we can &lt;strong&gt;randomize&lt;/strong&gt; the periods slightly by adding a variation of up to 10 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightOnPeriods&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Randomize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Using the Timeline
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Checking if the Lights Should Be On Right Now
&lt;/h4&gt;

&lt;p&gt;To check if the lights should be on at the current moment, you can simply use &lt;code&gt;IsNow()&lt;/code&gt; on the &lt;code&gt;lightOnPeriods&lt;/code&gt; timeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNow&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Turn lights on.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Turn lights off.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Enumerating Future (or Past) Events
&lt;/h4&gt;

&lt;p&gt;You can easily enumerate future or past periods to check when the lights will go on. For example, let’s find out when the lights will turn on during the &lt;strong&gt;rest of the current month&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"The rest of the current month the lights will go on at:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnumerateRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeZoneInstants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CurrentMonth&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But due to the dynamic nature of timelines we can just as easily see when the lights will turn on in &lt;strong&gt;February 2050&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"In February 2050 the lights will go on at:"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnumeratePeriod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeZonePeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2050&lt;/span&gt;&lt;span class="p"&gt;))){&lt;/span&gt;
    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Note that the period timeline &lt;strong&gt;only resolves the necessary periods when enumerated&lt;/strong&gt;, ensuring efficiency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Scheduling Automatic Actions
&lt;/h4&gt;

&lt;p&gt;To automate actions based on the timeline, you can use &lt;strong&gt;ReactiveX&lt;/strong&gt;, which provides a powerful way to handle event-driven programming. The &lt;code&gt;SubscribeStartEnd&lt;/code&gt; method internally utilizes an &lt;code&gt;IObservable&lt;/code&gt;, allowing you to schedule events reactively.&lt;/p&gt;

&lt;p&gt;By default, this method even evaluates the current state of the timeline, invoking the applicable method on startup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;lightOnPeriods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeStartEnd&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;TurnLightsOn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;TurnLightsOff&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach allows you to focus on what matters—like defining when you want your lights to turn on—without manually handling the timing and scheduling. As a result, your code becomes more &lt;strong&gt;intuitive&lt;/strong&gt;, &lt;strong&gt;dynamic&lt;/strong&gt;, and &lt;strong&gt;use case-driven&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Curious about more use cases? Check out Occurify’s &lt;a href="https://github.com/Occurify/Occurify?tab=readme-ov-file#potential-use-cases" rel="noopener noreferrer"&gt;README on Github&lt;/a&gt; or explore the &lt;a href="https://github.com/Occurify/Occurify/blob/main/examples/Occurify.Examples/ReadMeExamples.cs" rel="noopener noreferrer"&gt;example code&lt;/a&gt; for more.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;p&gt;Occurify uses 4 main concepts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Represented by&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Instant&lt;/td&gt;
&lt;td&gt;UTC &lt;code&gt;DateTime&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;A single instant in time.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Period&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Period&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A period of time, defined by a start and end instant.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instant timeline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ITimeline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A timeline containing instants.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Period timeline&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPeriodTimeline&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A timeline containing periods.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Instant
&lt;/h3&gt;

&lt;p&gt;An instant is represented using a &lt;code&gt;DateTime&lt;/code&gt; with &lt;code&gt;Kind&lt;/code&gt; set to &lt;code&gt;DateTimeKind.Utc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The valid range for an instant is from &lt;code&gt;01-01-0000&lt;/code&gt; to &lt;code&gt;31-12-9999&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Period
&lt;/h3&gt;

&lt;p&gt;A period is defined by two instants: a start and an end.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the start is null, it means the period has no defined beginning (i.e., it started at the beginning of time).&lt;/li&gt;
&lt;li&gt;If the end is null, it means the period has no defined end (i.e., it lasts indefinitely).&lt;/li&gt;
&lt;li&gt;If both start and end are null, the period is infinite in both directions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A period contains all instants that are greater than or equal to the start instant and smaller than the end instant:&lt;br&gt;
&lt;code&gt;(Start == null || instant &amp;gt;= Start) &amp;amp;&amp;amp;&lt;br&gt;
(End == null || instant &amp;lt; End)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Concept:&lt;/strong&gt; Consecutive periods (a period with the same start as the end of another) do not overlap, ensuring that each instant belongs to only one period.&lt;/p&gt;

&lt;h4&gt;
  
  
  Period Record:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;End&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IComparable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Different ways of creating a period:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Using extension methods&lt;/span&gt;
&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;nowToOneHoursFromNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToPeriodWithDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;nowToTwoHoursFromNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;nowToNeverEnding&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Using static methods&lt;/span&gt;
&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;nowToThreeHoursFromNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Instant Timeline
&lt;/h3&gt;

&lt;p&gt;An instant timeline represents a timeline of instants.&lt;/p&gt;

&lt;p&gt;Although an instant timeline implements &lt;code&gt;IEnumerable&amp;lt;DateTime&amp;gt;&lt;/code&gt;, it does not necessarily represent a collection of instants. Instead, it can represent the concept of a specific timeline. For example, an instant timeline can represent the concept of "all sunsets," without having to calculate or store all sunset times in memory. Instants in the timeline are only calculated when the timeline is enumerated or iterated over.&lt;/p&gt;

&lt;p&gt;Within Occurify, a timeline has the following properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immutable:&lt;/strong&gt; Once created, an instant timeline cannot be modified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic:&lt;/strong&gt; The timeline will always yield the same instants given the same parameters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works with UTC:&lt;/strong&gt; All instants in an instant timeline are represented as &lt;code&gt;DateTime&lt;/code&gt; values in UTC (&lt;code&gt;DateTimeKind.Utc&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An instant timeline implements &lt;code&gt;IEnumerable&amp;lt;DateTime&amp;gt;&lt;/code&gt;, meaning that you can enumerate through it to access all instants, starting from the earliest to the latest.&lt;/p&gt;

&lt;h4&gt;
  
  
  Timeline Interface:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;ITimeline&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;GetPreviousUtcInstant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcRelativeTo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;GetNextUtcInstant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcRelativeTo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsInstant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcDateTime&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Different ways of creating an instant pipeline:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Using extension methods&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;timeline1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsTimeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;timeline2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;AsTimeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Using static methods&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;timeline3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromInstants&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;timeline4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Periodic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that &lt;code&gt;timeline4&lt;/code&gt; is not a timeline with concrete instants. Only when reading it, will the instants be resolved. Simular to &lt;code&gt;Linq&lt;/code&gt; methods, filtering only wraps the timeline in a filter class. Instants will only be resolved by reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  Period Timeline
&lt;/h3&gt;

&lt;p&gt;A period timeline is defined by a start timeline and an end timeline and represents a timeline of periods.&lt;/p&gt;

&lt;p&gt;Periods on the period timeline start at any instant on the start timeline and end by the next first instant on the end timeline. Periods cannot overlap.&lt;/p&gt;

&lt;p&gt;Due to the nature of instant timelines, period timelines can also represent a concept, but in the form of a period. For example, a period timeline can represent a concept like "all periods between sunrise and sunset", by constructing it from an instant timeline that represents "all sunrises" and one that represents "all sunsets".&lt;/p&gt;

&lt;p&gt;If the earliest instant on both the start and end timelines is an end instant, the first period is assumed to have always started (start = &lt;code&gt;null&lt;/code&gt;). Simularly, if the last instant is a start instant, the last instant is assumed to never end (end = &lt;code&gt;null&lt;/code&gt;). If a period has no start and no end, it is empty. A period timeline cannot contain an infinite period.&lt;/p&gt;

&lt;p&gt;If there are more consecutive start instants, the earliest one defines the start of a period. In case there are consecutive end instants, the earliest one defines the end of a period.&lt;/p&gt;

&lt;p&gt;A period timeline also implements &lt;code&gt;IEnumerable&amp;lt;Period&amp;gt;&lt;/code&gt;. Enumerating a timeline will iterate all instants in a timeline from earliest to latest.&lt;/p&gt;

&lt;h4&gt;
  
  
  Period Timeline Interface:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;StartTimeline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;EndTimeline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Different ways of creating an period pipeline:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utcNow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utcNow&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Using extension methods on periods&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;periodTimeline1&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsPeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;periodTimeline2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;AsPeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Using extension methods on instant timelines&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;periodStartTimeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Timeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Periodic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;ITimeline&lt;/span&gt; &lt;span class="n"&gt;periodEndTimeline&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;periodStartTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OffsetMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;periodTimeline3&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;periodStartTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;To&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periodEndTimeline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Using static methods&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;periodTimeline4&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromPeriods&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;IPeriodTimeline&lt;/span&gt; &lt;span class="n"&gt;periodTimeline5&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PeriodTimeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Between&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;periodStartTimeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;periodEndTimeline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;You can explore the full source code on &lt;a href="https://github.com/Occurify/Occurify" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you find Occurify helpful or interesting, a ⭐️ on the repo would mean a lot!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>programming</category>
      <category>algorithms</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Supercharging Home Assistant Automations: Initial States and Boolean Logic for NetDaemon Rx</title>
      <dc:creator>Jasper</dc:creator>
      <pubDate>Tue, 28 Jan 2025 15:47:10 +0000</pubDate>
      <link>https://dev.to/devjaspernl/supercharging-home-assistant-automations-initial-states-and-boolean-logic-for-netdaemon-rx-3bd5</link>
      <guid>https://dev.to/devjaspernl/supercharging-home-assistant-automations-initial-states-and-boolean-logic-for-netdaemon-rx-3bd5</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As a software developer, I stepped into the home automation hobby relatively late (about 3 years ago). Being a .NET developer, it didn’t take me long to stumble upon NetDaemon, a great open source library using Home Assistant’s websocket API to allow .NET developers to write their automations in C#.&lt;br&gt;
It was also this library that introduced me to ReactiveX for handling event streams. I immediately loved this powerful combination; allowing the fluent interface to write compact yet surprisingly readable code, something every developer likes!&lt;/p&gt;

&lt;p&gt;Over time, as I refined my home automations, I developed patterns and libraries that significantly improved my implementations. This article is the first in a series where I’ll share these ideas, hoping to inspire other smart home enthusiasts.&lt;/p&gt;

&lt;p&gt;In this first installment, I’ll dive into implementation patterns I use frequently when working on my smart home. These patterns not only make code more readable but also lead to more robust automations. I’ve also included two supporting libraries with useful extension methods to help you apply these patterns effectively.&lt;/p&gt;

&lt;p&gt;If you’re a Home Assistant user looking to level up your automations, this article is for you!&lt;/p&gt;
&lt;h2&gt;
  
  
  State vs. State Changes: A Unified Approach
&lt;/h2&gt;

&lt;p&gt;Entities in Home Assistant (and NetDaemon) can be used to get the current state of an entity — for example, whether a light is on, its brightness level, or other attributes. Automations in Home Assistant rely on triggers, which execute actions based on changes in state. Similarly, NetDaemon lets you use observables to subscribe to these updates.&lt;/p&gt;

&lt;p&gt;Here’s a simple example from the NetDaemon documentation where a light is turned on when a binary sensor changes its state to “on”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ExampleAppHaContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IHaContext&lt;/span&gt; &lt;span class="n"&gt;ha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Entities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ha&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinarySensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OfficeMotion&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsOn&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Light&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Office&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works well for triggers like motion sensors or switches, where actions are tied to discrete events without much concern for the current state. For example, if someone presses a switch, you turn a light on or off.&lt;/p&gt;

&lt;p&gt;However, as I worked on my smart home, I realized that many automations rely on both the current state and state changes. Focusing solely on state changes can lead to unexpected behavior, particularly during system startups or power outages.&lt;/p&gt;

&lt;p&gt;Take these scenarios, for instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Curtains should be closed between sunset and sunrise and open otherwise.&lt;/li&gt;
&lt;li&gt;A closet light should turn on when the door is open and off when it’s closed.&lt;/li&gt;
&lt;li&gt;The hallway light should adjust based on multiple factors, such as the sun’s position, whether people are asleep, and the bedroom door's state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first, you might think using state changes is enough. For example, here’s how you could automate curtains based on the sun’s elevation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example will close the curtains when the sun goes below a certain elevation and will open them when it rises above. However, this is overlooking one important aspect of an entity: its current state. This might seem like a minor detail, as implementation using changes will cause the desired outcome to be “eventually correct”. However, this does influence the startup behavior of your smart home. Something that regularly occurs during development, but could also occur because of something external like a power outage.&lt;/p&gt;

&lt;p&gt;Imagine a power outage occurs just before the curtains are about to close, and the system restarts after the elevation of the sun is under the threshold, the curtains will remain open all night. Something you or your family will probably even interpret as a bug.&lt;/p&gt;

&lt;p&gt;And from a design perspective you could indeed argue that this is a bug: You want to align the state of the curtains with the state of the sun. Yet, this automation aligned the state of the curtains with changes in the state of the sun, not taking into account its current state. "Only state changes" and “current state followed by state changes” are different concepts.&lt;/p&gt;

&lt;p&gt;Whether you want to call this a bug doesn’t really matter. You probably think this is not a big deal and already figured out this issue is easy to solve! Simply evaluate the current state of the entity upon startup and all is well.&lt;/p&gt;

&lt;p&gt;And so, you end up with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Subscribe to state changes&lt;/span&gt;
    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Evaluate current state&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which might be cleaned up into something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;SunEntity&lt;/span&gt; &lt;span class="n"&gt;_sun&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;_coverEntities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_sun&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_coverEntities&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Subscribe to state changes&lt;/span&gt;
    &lt;span class="n"&gt;_sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
             &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;EvaluateCurtainStates&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="c1"&gt;// Evaluate current state&lt;/span&gt;
    &lt;span class="nf"&gt;EvaluateCurtainStates&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;EvaluateCurtainStates&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;CurtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;_coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;_coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;_coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code works well. It’s readable and does what it’s supposed to. However, after implementing logic using only state changes for a while, I began to appreciate the elegance of writing logic in a single, fluent line with Reactive Extensions. I wanted a way to represent both the entity's current state and its changes using observables, without sacrificing the fluency of Rx syntax.&lt;/p&gt;

&lt;p&gt;To achieve this, I started using &lt;code&gt;Prepend&lt;/code&gt; to add the entity’s current state to its state changes observable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sunState&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StateAllChanges&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Prepend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;StateChange&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SunEntity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SunAttributes&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Initially, there is only a new state, so old is null.&lt;/span&gt;
            &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityState&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I found myself using this structure often, I created the &lt;code&gt;Stateful&lt;/code&gt; and &lt;code&gt;StatefulAll&lt;/code&gt; extension methods for it. Allowing us to use the following implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StatefulAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StatefulAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Old&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this pattern allows us to keep writing our logic in the same clean fashion as when only subscribing to changes while still triggering on the entity’s initial state.&lt;/p&gt;

&lt;p&gt;Note: The implementations of &lt;code&gt;Stateful&lt;/code&gt; and &lt;code&gt;StatefulAll&lt;/code&gt; use &lt;code&gt;Observable.Defer&lt;/code&gt; so the most recent state is always reported upon subscribing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Note:&lt;/strong&gt; (10 Feb 2025) After reading this article, the NetDaemon team adopted my proposal for an observable that emits the current state upon subscribing. I implemented this in NetDaemon, and it was released in version 25.5.0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Leveraging Boolean Observables for Cleaner Code
&lt;/h2&gt;

&lt;p&gt;You might notice that in the examples of the previous chapter, I implement both the open and close cases using separate statements even though they are opposites of each other.&lt;/p&gt;

&lt;p&gt;Keeping them separate like this doesn’t make much sense from a coding perspective—it makes the implementation prone to mistakes; it is easy to overlook one of the two conditions while updating the other. Let’s improve this:&lt;/p&gt;

&lt;p&gt;If we take a look at the functionality, all we really want is a trigger when the sun is/goes up and when the sun is/goes down. As this translates to either a &lt;code&gt;true&lt;/code&gt; or a &lt;code&gt;false&lt;/code&gt; we use the &lt;code&gt;Select&lt;/code&gt; method to convert our statement into a boolean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StatefulAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sunUp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sunUp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem with this implementation, is that we don’t filter based on the old state of the sun, leaving us with duplicate triggers. Luckily ReactiveX is here to help us. Instead of checking for a specific change in the condition itself, we can utilize ReactiveX’s &lt;code&gt;DistinctUntilChanged&lt;/code&gt; extension method to filter out any duplicate results, bringing us this implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StatefulAll&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DistinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sunUp&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sunUp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As this pattern of combining a &lt;code&gt;Select&lt;/code&gt; and &lt;code&gt;DistinctUntilChanged&lt;/code&gt; to form a boolean observable (&lt;code&gt;Observable&amp;lt;bool&amp;gt;&lt;/code&gt;) is so useful, we can turn it into a &lt;code&gt;ToBooleanObservable&lt;/code&gt; extension method. And as there are now only two possible values (&lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;) emitted from our observable, we can use an extension method like &lt;code&gt;SubscribeTrueFalse&lt;/code&gt; for additional convenience and readability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;Example&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SunEntities&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CoverEntities&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;curtainCloseSunElevation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrueFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there we have it—a concise, readable piece of code that does exactly what we need. It triggers the correct automation when our application starts up, and it doesn’t contain any duplicate logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying Patterns to Real-World Scenarios
&lt;/h2&gt;

&lt;p&gt;Of course, the examples in the previous chapters are intentionally simple for the sake of illustration. Real-world use cases are rarely this straightforward. Often, multiple states need to be interpreted together, and there can be various possible outcomes depending on these states. While that’s true, I want to show you how these patterns shine &lt;em&gt;especially&lt;/em&gt; in handling more complex cases.&lt;/p&gt;

&lt;p&gt;By using the first pattern—adding the current state to the observable we are working with—we not only ensure that our automations trigger correctly from the start but also allow ourselves to leverage ReactiveX to combine, filter, and evaluate multiple observables upon startup. This becomes especially useful when dealing with more intricate conditions and behaviors.&lt;/p&gt;

&lt;p&gt;When we introduce the second pattern, which leverages boolean observables, we can easily create helper methods for logical operators and scheduling. These methods make filtering more human-readable, resulting in a much better coding experience.&lt;/p&gt;

&lt;p&gt;Combining these patterns makes for a coding experience that comes very close to explaining the use cases themselves.&lt;/p&gt;

&lt;p&gt;Rather than explaining this in more detail, let’s dive into some real examples from my own smart home. These examples will showcase how these implementation patterns map directly to real-world use cases. While I’ve simplified a few of them for clarity, these are actual scenarios from my home automation setup:&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing Bedroom Curtains (Logical Operators)
&lt;/h3&gt;

&lt;p&gt;In addition to using stateful boolean observables, this example demonstrates how using &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt; allows us to apply logical operators (such as &lt;code&gt;Or&lt;/code&gt; in this case) for clean and concise code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nightTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;anyoneAsleep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JasperAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnonAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;nightTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anyoneAsleep&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrueFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedroomRollerShutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CloseCover&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedroomRollerShutter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenCover&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this implementation, you can be confident that the curtains will close when it is nighttime &lt;em&gt;or&lt;/em&gt; when anyone is asleep. Even if the automation starts up during nighttime, it will close the curtains if they are not already closed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Closet Light (Scheduling)
&lt;/h3&gt;

&lt;p&gt;In this example, we turn on the closet light when the door is opened, but only if no one is asleep. However, if the door is left open for too long, the light automatically turns off.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;noOneAsleep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JasperAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnonAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;Not&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;closetDoorOpenShorterThanFiveMin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binarySensorEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedroomClosetDoorSensorContact&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOpenClosedObservable&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;LimitTrueDuration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;noOneAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;And&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;closetDoorOpenShorterThanFiveMin&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrueFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClosetLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClosetLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOff&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This example demonstrates the use the method alias &lt;code&gt;ToOpenClosedObservable&lt;/code&gt; for more readable code, as well as the use of scheduling extension methods like &lt;code&gt;LimitTrueDuration&lt;/code&gt; (from &lt;code&gt;Reactive.Booleans&lt;/code&gt;) on &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Additionally, this example highlights the extra power of &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt; over &lt;code&gt;IObservable&amp;lt;T&amp;gt;&lt;/code&gt;. A method like &lt;code&gt;LimitTrueDuration&lt;/code&gt; is far simpler to implement with &lt;code&gt;bool&lt;/code&gt; because the result we expect after the timeout is inherently clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upstairs Hallway Light (Multiple Output States)
&lt;/h3&gt;

&lt;p&gt;Even for more complex use cases, boolean observables continue to provide value. Consider the example where we want the upstairs hallway light to be triggered by motion. During the day, the light should turn on at full brightness; at night, it should be dimmed. We also want to dim the light during the day if anyone is taking a nap and the bedroom door is open.&lt;/p&gt;

&lt;p&gt;In this case, we can use the &lt;code&gt;CombineLatest&lt;/code&gt; extension method from &lt;code&gt;System.Reactive.Linq&lt;/code&gt; to evaluate multiple states together. Since we’re using stateful observables here, we don't need to worry about startup edge cases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nightTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;anyoneAsleep&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JasperAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputBooleanEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AnonAsleep&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bedroomDoorOpen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;binarySensorEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BedroomDoorSensorContact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOpenClosedObservable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;motionSensorTriggered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;upstairsHallwayMotionSensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateMotionBrightnessDelayObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="n"&gt;nightTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anyoneAsleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bedroomDoorOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;motionSensorTriggered&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;Subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tuple&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Unpacking the tuple into named variables for better readability.&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;nightTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;anyoneAsleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bedroomDoorOpen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;motionSensorTriggered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;motionSensorTriggered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpstairsHallwayLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOff&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lowBrightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nightTime&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;anyoneAsleep&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bedroomDoorOpen&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UpstairsHallwayLight&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;LightTurnOnParameters&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Brightness&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lowBrightness&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;255&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;upstairsHallwayMotionSensor&lt;/code&gt; in this example is a custom class that combines the motion and brightness sensor entities of a motion sensor. The implementation of &lt;code&gt;CreateMotionBrightnessDelayObservable&lt;/code&gt; is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;IObservable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateMotionBrightnessDelayObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;brightnessThreshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt; &lt;span class="n"&gt;delayAfterNoMotion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;motionLastXTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MotionOccupancySensor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PersistOnFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delayAfterNoMotion&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;brightnessLessThanX&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MotionIlluminanceLuxSensor&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;brightnessThreshold&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;triggered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;motionLastXTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CombineLatest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightnessLessThanX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;motionTriggered&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;brightnessTriggered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;motionTriggered&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;brightnessTriggered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;triggered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;motionTriggered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;triggered&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;triggered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;DistinctUntilChanged&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that while boolean observables are convenient, we can still leverage &lt;code&gt;CombineLatest&lt;/code&gt; with observables of other types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Christmas Tree Light Plug (Checking Entity Availability)
&lt;/h3&gt;

&lt;p&gt;As a final example, I want to demonstrate &lt;code&gt;RepeatWhenEntitiesBecomeAvailable&amp;lt;T&amp;gt;&lt;/code&gt;. This method repeats the last result of the observable it is applied to whenever one or more entities become available again. Here, it is used with a power plug for my Christmas tree, which only gets plugged in during the holidays. This implementation ensures that when the plug is powered, it turns on the lights as soon as Home Assistant reconnects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;nightTime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sunEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sun&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBooleanObservable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Elevation&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;nightTimeSunElevation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;nightTime&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RepeatWhenEntitiesBecomeAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;switchEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomChristmasTreeLights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrueFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;switchEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomChristmasTreeLights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;switchEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomChristmasTreeLights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOff&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my opinion the logic of &lt;code&gt;RepeatWhenEntitiesBecomeAvailable&amp;lt;T&amp;gt;&lt;/code&gt; could be applied to any automation, but if you do, it might be worth applying this kind of logic to a different layer in your application. For now I've added the method in &lt;code&gt;NetDaemon.Extensions.Observables&lt;/code&gt; so you can easily take advantage of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;While these patterns may seem like small tweaks to how we approach our implementations, the impact they can have is far-reaching. By transforming our entities into stateful event streams and leveraging boolean observables, we unlock a new level of readability and maintainability in our code.&lt;/p&gt;

&lt;p&gt;The power of these patterns is amplified when combined with boolean logic and boolean specific scheduling, as demonstrated throughout this article. They allow us to express complex automation scenarios with clarity, making it easier to reason about and extend our automations over time.&lt;/p&gt;

&lt;p&gt;Whether you’re a seasoned developer or just starting out with Home Assistant and NetDaemon, adopting these practices can help you write smarter, more reactive automations that scale with your smart home’s growing complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Libraries
&lt;/h2&gt;

&lt;p&gt;For readers who want to implement these concepts, the following libraries provide convenient extension methods and tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The methods discussed throughout this article are available in the &lt;code&gt;NetDaemon.Extensions.Observables&lt;/code&gt; library on &lt;a href="https://github.com/DevJasperNL/NetDaemon.Extensions.Observables" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. You can also find it on &lt;a href="https://www.nuget.org/packages/DevJasper.NetDaemon.Extensions.Observables" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;As the boolean logic is not dependent on NetDaemon and can be applied to any implementation of &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt;, I created a separate library called &lt;code&gt;Reactive.Booleans&lt;/code&gt; on &lt;a href="https://github.com/DevJasperNL/Reactive.Boolean" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, also available on &lt;a href="https://www.nuget.org/packages/Reactive.Boolean" rel="noopener noreferrer"&gt;NuGet&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both libraries are open-source and licensed under the MIT license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design Decisions &amp;amp; Future Plans
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Although scheduling is primarily managed by the &lt;code&gt;Reactive.Boolean&lt;/code&gt; library, understanding the Entity can enhance certain scheduling methods. When calling &lt;code&gt;WhenTrueFor&lt;/code&gt; or &lt;code&gt;LimitTrueDuration&lt;/code&gt; directly on an Entity rather than on an &lt;code&gt;IObservable&amp;lt;bool&amp;gt;&lt;/code&gt;, both &lt;code&gt;LastChanged&lt;/code&gt; and the passage of time are utilized to determine whether a true value is emitted. For the sake of simplicity, the examples in this article do not demonstrate this behavior.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;NetDaemon.Extensions.Observables&lt;/code&gt; library now contains aliases for both on/off and open/close states. I chose these specific combinations because they are the ones I use most frequently in my automations. That said, I encourage you to create aliases in your own code to improve code readability for your specific use cases.&lt;/li&gt;
&lt;li&gt;Given the parameterless &lt;code&gt;ToBooleanObservable&lt;/code&gt; extension method, I considered adding boolean extension methods for entities (e.g. &lt;code&gt;IObservable&amp;lt;bool&amp;gt; And(this Entity entity1, Entity entity2)&lt;/code&gt;). Even though this could make code even more compact, I decided against this for now as I believe it makes code less readable. I think requiring developers to first call &lt;code&gt;ToBooleanObservable&lt;/code&gt; (or an alias) forces cleaner and more intentional code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;frontWindowCurtainOpen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOpenClosedObservable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;backWindowCurtainOpen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToOpenClosedObservable&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;frontWindowCurtainOpen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backWindowCurtainOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrue&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomLights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOff&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is clearer than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomFrontWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coverEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomBackWindowCurtain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SubscribeTrue&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lightEntities&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LivingRoomLights&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TurnOff&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the second example, it’s less obvious whether the observable emits &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt; when the curtain is open. That said, I recognize that some entity names (e.g. &lt;code&gt;binarySensorEntities.BedroomClosetDoorSensorContact&lt;/code&gt;) are clearer, and I might revisit this decision later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RepeatWhenEntitiesBecomeAvailable&lt;/code&gt; now works with two hardcoded constants (&lt;code&gt;unknown&lt;/code&gt;, &lt;code&gt;unavailable&lt;/code&gt;) as I didn't want to overengineer the library just for this one method. Eventually I might make these configurable, or even make a .Net Core implementation of the library allowing configuration on the &lt;code&gt;IHostBuilder&lt;/code&gt; for example.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Reactive.Booleans&lt;/code&gt; implements an alias for the &lt;code&gt;And&lt;/code&gt; method called &lt;code&gt;AndOp&lt;/code&gt;. This was created to avoid namespace conflicts with the &lt;code&gt;And&lt;/code&gt; method in reactive joins.&lt;/li&gt;
&lt;li&gt;NetDaemon provides a &lt;code&gt;SubscribeSafe&lt;/code&gt; method which prevents exceptions from unsubscribing from the observable. I plan to extend &lt;code&gt;NetDaemon.Extensions.Observables&lt;/code&gt; to include this functionality as well.&lt;/li&gt;
&lt;li&gt;My personal smart home solution includes many other useful extension methods for working with entities and boolean observables. Although I considered adding them to the libraries, I decided against it for now. The libraries currently align well with the content of this article, and I wanted to avoid cluttering them. In the future, I’ll likely update the libraries and mention these changes here.&lt;/li&gt;
&lt;li&gt;For the scheduling extension methods in the &lt;code&gt;Reactive.Booleans&lt;/code&gt; library in particular there are some additional methods I can think of and will probably add at some point. For now I included the ones I used in my automations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Article Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;10 Feb 2025: Added note explaining that NetDaemon adopted the idea of emitting current state in an observable.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>homeassistant</category>
      <category>programming</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
